From 065f07bc58d25a7e62ebd075389ebdc8db61cf25 Mon Sep 17 00:00:00 2001 From: Maxwell Jeffress Date: Sat, 8 Nov 2025 12:27:38 +0000 Subject: [PATCH] Autopositioning, custom fonts --- README.md | 4 +- example/example.cpp | 37 ++----- src/quip.cpp | 162 +++++++++++++++++++++------- src/quip.h | 252 +++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 372 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index ce72dd4..7d4ef44 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,9 @@ sudo cmake --install build int main() { Quip::Window window("Quip Window", 800, 600); - window.addChild("textLabel", Quip::Widget::TextLabel("Hello from Quip!", 10, 10)); + window.addChild("textLabel", Quip::Widget::TextLabel("Hello from Quip!")); return window.run(); } ``` -Documentation is provided in the repository wiki. \ No newline at end of file +Documentation is provided in the repository wiki. diff --git a/example/example.cpp b/example/example.cpp index de25ee9..b175b40 100644 --- a/example/example.cpp +++ b/example/example.cpp @@ -1,42 +1,17 @@ #include int main() { - bool goingDown = true, goingRight = true; - Quip::Window window("Quip Window", 800, 600, [&window, &goingDown, &goingRight]() { - auto* imagewidget = window.getChild("image"); - auto& image = std::get(*imagewidget); - if (goingDown) { - image.ypos ++; - if (image.ypos + image.height > window.height) { - goingDown = false; - } - } else { - image.ypos --; - if (image.ypos < 0) { - goingDown = true; - } - } - if (goingRight) { - image.xpos ++; - if (image.xpos + image.width > window.width) { - goingRight = false; - } - } else { - image.xpos --; - if (image.xpos < 0) { - goingRight = true; - } - } - }); int buttonPresses = 0; - window.addChild("textLabel", Quip::Widget::TextLabel("Hi there!", 10, 10)); - window.addChild("button", Quip::Widget::Button("Click me!", [&window, &buttonPresses]() { + Quip::Window window("Quip Window", 800, 600); + Quip::Font font("/usr/share/fonts/noto/NotoSans-Regular.ttf", 16); + window.addChild("image", Quip::Widget::Image("dingus.png", 100, 100)); + window.addChild("textLabel", Quip::Widget::TextLabel("Hi there!", font)); + window.addChild("button", Quip::Widget::Button(Quip::Widget::TextLabel("Click me!", font), [&window, &buttonPresses] { auto* widget = window.getChild("textLabel"); if (widget && std::holds_alternative(*widget)) { buttonPresses++; std::get(*widget).text = "You have clicked the button " + std::to_string(buttonPresses) + " times!"; } - }, 10, 50)); - window.addChild("image", Quip::Widget::Image("dingus.png", 300, 300, 100, 100)); + })); return window.run(); } diff --git a/src/quip.cpp b/src/quip.cpp index b478f06..95a06cc 100644 --- a/src/quip.cpp +++ b/src/quip.cpp @@ -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(xpos), static_cast(ypos), static_cast(surface->w), static_cast(surface->h)}; + SDL_FRect destRect; + destRect = {static_cast(10), static_cast(customY), static_cast(surface->w), static_cast(surface->h)}; + if (customY == -1) { + destRect.y += static_cast(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(xpos), static_cast(ypos), static_cast(width), static_cast(height)}; + SDL_FRect destRect = {static_cast(10), static_cast(window.getYPos(height)), static_cast(width), static_cast(height)}; SDL_RenderTexture(window.renderer, texture, nullptr, &destRect); SDL_DestroyTexture(texture); } - Button::Button(std::string text, std::function 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(xpos), static_cast(ypos), static_cast(width), static_cast(height)}; + Button::Button(TextLabel text, std::function 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(10), static_cast(renderYPos), static_cast(width), static_cast(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(xpos), static_cast(ypos), static_cast(surface->w), static_cast(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(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(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(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& 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& 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)) { - Widget::Button& button = std::get(widget); + auto& button = std::get(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)) { std::get(widget).render(*this); } else if (std::holds_alternative(widget)) { @@ -187,7 +276,6 @@ namespace Quip { cleanup: windowCount --; if (windowCount == 0) { - TTF_CloseFont(font); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); diff --git a/src/quip.h b/src/quip.h index 8e65194..3a20a7b 100644 --- a/src/quip.h +++ b/src/quip.h @@ -2,60 +2,286 @@ #include #include -#include #include #include #include #include +#include +/** + * @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 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 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 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 callback); }; + /** + * @brief Variant type that can hold any widget type. + * + * Used for storing heterogeneous collections of widgets in a Window. + */ typedef std::variant 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 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 widgetIds; + + /** @brief Vector containing all widgets added to this window. */ + std::vector widgets; + + /** @brief Optional callback function called each frame before rendering. */ std::function 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& onFrame = nullptr); + + /** + * @brief Default constructor for Window. + * + * Creates an uninitialized window. Must be properly initialized before use. + */ Window(); };