Initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
build
|
||||||
|
cmake-build-debug
|
||||||
|
.idea
|
||||||
55
CMakeLists.txt
Normal file
55
CMakeLists.txt
Normal file
@@ -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
|
||||||
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
|
||||||
|
$<INSTALL_INTERFACE:include/quip>
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
9
Config.cmake.in
Normal file
9
Config.cmake.in
Normal file
@@ -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)
|
||||||
36
README.md
Normal file
36
README.md
Normal file
@@ -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 <quip.h>
|
||||||
|
|
||||||
|
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.
|
||||||
1
example/.gitignore
vendored
Normal file
1
example/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
build
|
||||||
10
example/CMakeLists.txt
Normal file
10
example/CMakeLists.txt
Normal file
@@ -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)
|
||||||
15
example/example.cpp
Normal file
15
example/example.cpp
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#include <quip.h>
|
||||||
|
|
||||||
|
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<Quip::Widget::TextLabel>(*widget)) {
|
||||||
|
buttonPresses++;
|
||||||
|
std::get<Quip::Widget::TextLabel>(*widget).text = "You have clicked the button " + std::to_string(buttonPresses) + " times!";
|
||||||
|
}
|
||||||
|
}, 10, 50));
|
||||||
|
return window.run();
|
||||||
|
}
|
||||||
165
src/quip.cpp
Normal file
165
src/quip.cpp
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3_ttf/SDL_ttf.h>
|
||||||
|
#include <iostream>
|
||||||
|
#include <utility>
|
||||||
|
#include <variant>
|
||||||
|
#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<void()> 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::Button>(widget)) {
|
||||||
|
Widget::Button& 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) {
|
||||||
|
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::TextLabel>(widget)) {
|
||||||
|
const auto& textLabel = std::get<Widget::TextLabel>(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<float>(textLabel.xpos), static_cast<float>(textLabel.ypos), static_cast<float>(surface->w), static_cast<float>(surface->h)};
|
||||||
|
SDL_RenderTexture(renderer, texture, nullptr, &destRect);
|
||||||
|
SDL_DestroyTexture(texture);
|
||||||
|
SDL_DestroySurface(surface);
|
||||||
|
} else if (std::holds_alternative<Widget::Button>(widget)) {
|
||||||
|
const auto& button = std::get<Widget::Button>(widget);
|
||||||
|
SDL_SetRenderDrawColor(renderer, 40, 60, 80, 255); // Dark gray background
|
||||||
|
SDL_FRect backgroundRect = {static_cast<float>(button.xpos), static_cast<float>(button.ypos), static_cast<float>(button.width), static_cast<float>(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<float>(button.xpos), static_cast<float>(button.ypos), static_cast<float>(surface->w), static_cast<float>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/quip.h
Normal file
49
src/quip.h
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3_ttf/SDL_ttf.h>
|
||||||
|
#include <string>
|
||||||
|
#include <variant>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
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<void()> callback;
|
||||||
|
int xpos, ypos = 0;
|
||||||
|
int width = 100, height = 30;
|
||||||
|
explicit Button(std::string text, std::function<void()> callback, int xpos, int ypos);
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef std::variant<TextLabel, Button> Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Window {
|
||||||
|
SDL_Window* window;
|
||||||
|
SDL_Renderer* renderer;
|
||||||
|
SDL_Event event;
|
||||||
|
TTF_Font* font;
|
||||||
|
std::string title;
|
||||||
|
int width, height;
|
||||||
|
std::map<std::string, Widget::Widget> 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();
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user