From a7b578636e0ffb99cdecf2463cc04b639e83b46c Mon Sep 17 00:00:00 2001 From: Maxwell Jeffress Date: Sat, 1 Nov 2025 15:54:37 +0000 Subject: [PATCH] Initial commit --- .gitignore | 3 + CMakeLists.txt | 55 ++++++++++++++ Config.cmake.in | 9 +++ README.md | 36 +++++++++ example/.gitignore | 1 + example/CMakeLists.txt | 10 +++ example/example.cpp | 15 ++++ src/quip.cpp | 165 +++++++++++++++++++++++++++++++++++++++++ src/quip.h | 49 ++++++++++++ 9 files changed, 343 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 Config.cmake.in create mode 100644 README.md create mode 100644 example/.gitignore create mode 100644 example/CMakeLists.txt create mode 100644 example/example.cpp create mode 100644 src/quip.cpp create mode 100644 src/quip.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d6ded8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build +cmake-build-debug +.idea diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..885e8c8 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,55 @@ +cmake_minimum_required(VERSION 3.15) +project(quip VERSION 0.0.1) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_library(quip SHARED src/quip.cpp src/quip.h) + +target_link_libraries(quip PUBLIC SDL3 SDL3_ttf) + +target_include_directories(quip PUBLIC + $ + $ +) + +set_target_properties(quip PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 1 + PUBLIC_HEADER "src/quip.h" +) + +include(GNUInstallDirs) + +install(TARGETS quip + EXPORT quipTargets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/quip +) + +install(EXPORT quipTargets + FILE quipTargets.cmake + NAMESPACE quip:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/quip +) + +include(CMakePackageConfigHelpers) +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/quipConfig.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/quip +) + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/quipConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/quipConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/quipConfigVersion.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/quip +) \ No newline at end of file diff --git a/Config.cmake.in b/Config.cmake.in new file mode 100644 index 0000000..f4fcb35 --- /dev/null +++ b/Config.cmake.in @@ -0,0 +1,9 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(SDL3 REQUIRED) +find_dependency(SDL3_ttf REQUIRED) + +include("${CMAKE_CURRENT_LIST_DIR}/quipTargets.cmake") + +check_required_components(quip) diff --git a/README.md b/README.md new file mode 100644 index 0000000..739d876 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# Quip + +Quip is a UI library that wraps around SDL3. + +## Building and Installing + +First, ensure you have SDL3, SDL3_ttf, and Noto fonts installed. + +On Arch Linux: + +```shell +paru -S sdl3 sdl3_ttf noto-fonts +``` + +The following commands will build and run Quip on your system. It will be installed to /usr/local/lib and /usr/local/include. + +```shell +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build +sudo cmake --install build +``` + +## Quickstart + +```c++ +#include + +int main() { + Quip::Window window("Quip Window", 800, 600); + int buttonPresses = 0; + window.addChild("textLabel", Quip::Widget::TextLabel("Hello from Quip!", 10, 10)); + return window.run(); +} +``` + +Documentation is provided in the repository wiki. \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1 @@ +build diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt new file mode 100644 index 0000000..fcb676b --- /dev/null +++ b/example/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 4.0) +project(example) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(quip REQUIRED) + +add_executable(example example.cpp) +target_link_libraries(example PRIVATE quip::quip) diff --git a/example/example.cpp b/example/example.cpp new file mode 100644 index 0000000..44cada0 --- /dev/null +++ b/example/example.cpp @@ -0,0 +1,15 @@ +#include + +int main() { + Quip::Window window("Quip Window", 800, 600); + int buttonPresses = 0; + window.addChild("textLabel", Quip::Widget::TextLabel("Hi there!", 10, 10)); + window.addChild("button", Quip::Widget::Button("Click me!", [&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)); + return window.run(); +} diff --git a/src/quip.cpp b/src/quip.cpp new file mode 100644 index 0000000..7105689 --- /dev/null +++ b/src/quip.cpp @@ -0,0 +1,165 @@ +#include +#include +#include +#include +#include +#include "quip.h" + +namespace Quip { + int windowCount = 0; + + namespace Widget { + TextLabel::TextLabel() = default; + TextLabel::TextLabel(std::string text, int xpos, int ypos) : text(std::move(text)), xpos(xpos), ypos(ypos) {} + + Button::Button(std::string text, std::function callback, int xpos, int ypos) : text(std::move(text)), callback(std::move(callback)), xpos(xpos), ypos(ypos) { + + } + + } + + Window::Window() = default; + Window::Window(std::string title, int width, int height) : title(std::move(title)), width(width), height(height) { + windowCount ++; + if (width <= 0 || height <= 0) { + error(2, "width is " + std::to_string(width) + " and height is " + std::to_string(height)); + return; + } + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + error(1, SDL_GetError()); + return; + } + 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()); + } + } + + void Window::error(int errCode, const std::string& context) { + SDL_DestroyWindow(window); + SDL_DestroyRenderer(renderer); + SDL_Quit(); + std::cerr << "SuperGUI::Window() error " << errCode << ": "; + switch (errCode) { + default: + case 0: + std::cerr << "Generic error (please raise an issue on Chookspace)" << std::endl; + break; + case 1: + std::cerr << "SDL error" << std::endl; + break; + case 2: + std::cerr << "Window size error" << std::endl; + break; + case 3: + std::cerr << "Widget name error" << std::endl; + break; + } + if (!context.empty()) { + std::cerr << "Context: " << context << std::endl; + } + switch (errCode) { + case 0: + throw std::runtime_error("SuperGUI::Window() Generic error"); + break; + case 1: + throw std::runtime_error("SuperGUI::Window() SDL error"); + break; + case 2: + throw std::runtime_error("SuperGUI::Window() Window size error"); + break; + case 3: + throw std::runtime_error("SuperGUI::Window() Widget name error"); + break; + } + } + + void Window::addChild(const std::string& id, const Widget::Widget& widget) { + if (id.empty()) { + error(3, "Widget ID is empty"); + } + widgets[id] = widget; + } + + Widget::Widget* Window::getChild(const std::string& id) { + if (widgets.find(id) == widgets.end()) { + error(3, "Cannot find widget with id " + id); + return nullptr; + } + return &widgets[id]; + } + + int Window::run() { + while (true) { + 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; + + // Check if click is inside any button + for (auto& [id, widget] : widgets) { + if (std::holds_alternative(widget)) { + Widget::Button& 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) { + if (button.callback) { + button.callback(); + } + } + } + } + } + } + } + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderClear(renderer); + for (auto const& [id, widget] : widgets) { + if (std::holds_alternative(widget)) { + const auto& textLabel = std::get(widget); + constexpr SDL_Color color = {255, 255, 255, 255}; + SDL_Surface* surface = TTF_RenderText_Blended(font, textLabel.text.c_str(), 0, color); + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); + SDL_FRect destRect = {static_cast(textLabel.xpos), static_cast(textLabel.ypos), static_cast(surface->w), static_cast(surface->h)}; + SDL_RenderTexture(renderer, texture, nullptr, &destRect); + SDL_DestroyTexture(texture); + SDL_DestroySurface(surface); + } else if (std::holds_alternative(widget)) { + const auto& button = std::get(widget); + SDL_SetRenderDrawColor(renderer, 40, 60, 80, 255); // Dark gray background + SDL_FRect backgroundRect = {static_cast(button.xpos), static_cast(button.ypos), static_cast(button.width), static_cast(button.height)}; + SDL_RenderFillRect(renderer, &backgroundRect); + constexpr SDL_Color color = {255, 255, 255, 255}; + SDL_Surface* surface = TTF_RenderText_Blended(font, button.text.c_str(), 0, color); + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); + SDL_FRect destRect = {static_cast(button.xpos), static_cast(button.ypos), static_cast(surface->w), static_cast(surface->h)}; + SDL_RenderTexture(renderer, texture, nullptr, &destRect); + SDL_DestroyTexture(texture); + SDL_DestroySurface(surface); + } + } + if (!SDL_RenderPresent(renderer)) { + error(1, SDL_GetError()); + } + } + cleanup: + windowCount --; + if (windowCount == 0) { + TTF_CloseFont(font); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + } + return 0; + } +} \ No newline at end of file diff --git a/src/quip.h b/src/quip.h new file mode 100644 index 0000000..6bc1bd2 --- /dev/null +++ b/src/quip.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Quip { + extern int windowCount; + namespace Widget { + class TextLabel { + public: + std::string text; + int xpos, ypos = 0; + explicit TextLabel(std::string text, int xpos, int ypos); + TextLabel(); + }; + class Button { + public: + std::string text; + std::function callback; + int xpos, ypos = 0; + int width = 100, height = 30; + explicit Button(std::string text, std::function callback, int xpos, int ypos); + }; + + typedef std::variant Widget; + } + + class Window { + SDL_Window* window; + SDL_Renderer* renderer; + SDL_Event event; + TTF_Font* font; + std::string title; + int width, height; + std::map widgets; + void error(int errCode = 0, const std::string& context = ""); + public: + int run(); + void addChild(const std::string& id, const Widget::Widget& widget); + Widget::Widget* getChild(const std::string& id); + explicit Window(std::string title, int width = 800, int height = 600); + Window(); + }; + +} \ No newline at end of file