Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions samples/main.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
MIT License

Copyright (c) 2022-2025 Kim Kulling
Copyright (c) 2022-2024 Kim Kulling

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand All @@ -21,12 +21,13 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include <iostream>
#include "widgets.h"

using namespace tinyui;

static constexpr Id RootPanelId = 1; /// The id for the root panel
static constexpr Id RootPanelId = 1;

static constexpr Id NextPanelId = 100;

int quit(uint32_t, void *instance) {
if (instance == nullptr) {
Expand Down Expand Up @@ -61,7 +62,7 @@ int main(int argc, char *argv[]) {
return ErrorCode;
}

Widgets::panel(ctx, RootPanelId, 0, "Sample-Dialog", Rect(90, 5, 120, 400), nullptr);
Widgets::panel(ctx, RootPanelId, 0, "Sample-Dialog", Rect(90, 5, 120, 500), nullptr);
Widgets::label(ctx, 2, RootPanelId, "Title", Rect(100, 10, 100, 20), Alignment::Center);
Widgets::button(ctx, 3, RootPanelId, "Test 1", nullptr, Rect(100, 50, 100, 40), nullptr);
Widgets::button(ctx, 4, RootPanelId, "Test 2", nullptr, Rect(100, 100, 100, 40), nullptr);
Expand All @@ -74,6 +75,8 @@ int main(int argc, char *argv[]) {
CallbackI updateProgressBarCallback(updateProgressbar, nullptr, Events::UpdateEvent);
Widgets::progressBar(ctx, 8, RootPanelId, Rect(100, 300, 100, 40), 50, &updateProgressBarCallback);

Widgets::textfield(ctx, 9, RootPanelId, Rect(100, 350, 100, 40), Alignment::Left);

while (TinyUi::run(ctx)) {
TinyUi::beginRender(ctx, style.mClearColor);
Widgets::renderWidgets(ctx);
Expand Down
2 changes: 1 addition & 1 deletion src/backends/sdl2_iodevice.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
MIT License

Copyright (c) 2022-2025 Kim Kulling
Copyright (c) 2022-2024 Kim Kulling

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
7 changes: 1 addition & 6 deletions src/backends/sdl2_iodevice.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
MIT License

Copyright (c) 2022-2025 Kim Kulling
Copyright (c) 2022-2024 Kim Kulling

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -31,12 +31,7 @@ namespace tinyui {
///
/// IO-Devices are used to contrl any kind of input / output operations.
struct IODevice {
/// @brief The class destructor.
~IODevice() = default;

/// @brief will do the update for the input states.
/// @param[out] event The new event.
/// @return true, if successful, false if not.
static bool update(SDL_Event &event);
};

Expand Down
15 changes: 9 additions & 6 deletions src/backends/sdl2_renderer.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
MIT License

Copyright (c) 2022-2025 Kim Kulling
Copyright (c) 2022-2024 Kim Kulling

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand All @@ -27,7 +27,6 @@ SOFTWARE.

#include <cassert>
#include <iostream>
#include <string_view>

namespace tinyui {
namespace {
Expand Down Expand Up @@ -63,8 +62,8 @@ void showDriverInUse(const Context &ctx) {
printDriverInfo(info);
}

int queryDriver(const Context &ctx, const std::string_view &driverType) {
if (driverType.empty()) {
int queryDriver(const Context &ctx, const char *driverType, size_t maxLen) {
if (driverType == nullptr) {
ctx.mLogger(LogSeverity::Error, "Driver type is a nullptr.");
return -1;
}
Expand All @@ -74,7 +73,11 @@ int queryDriver(const Context &ctx, const std::string_view &driverType) {
for (int i = 0; i < numRenderDrivers; ++i) {
SDL_RendererInfo info;
SDL_GetRenderDriverInfo(i, &info);
if (driverType == std::string(info.name)) {
size_t len = strlen(driverType);
if (len > maxLen) {
len = maxLen;
}
if (strncmp(driverType, info.name, len) == 0) {
found = i;
break;
}
Expand Down Expand Up @@ -264,7 +267,7 @@ ret_code Renderer::initScreen(Context &ctx, int32_t x, int32_t y, int32_t w, int
return ErrorCode;
}

const int driverIndex = queryDriver(ctx, std::string("opengl"));
const int driverIndex = queryDriver(ctx, "opengl", 256);
if (driverIndex == -1) {
ctx.mLogger(LogSeverity::Error, "Cannot open opengl driver");
return ErrorCode;
Expand Down
8 changes: 3 additions & 5 deletions src/backends/sdl2_renderer.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
MIT License

Copyright (c) 2022-2025 Kim Kulling
Copyright (c) 2022-2024 Kim Kulling

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -36,10 +36,9 @@ struct SDL_Renderer;
struct SDL_Texture;

namespace tinyui {

/// @brief Implementation of a SDL2 surface.

struct SurfaceImpl {
SDL_Surface *mSurface = nullptr;
SDL_Surface *mSurface;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Removed nullptr initialization for mSurface.

The explicit nullptr initialization for the mSurface member was removed. While the destructor and clear() method check for nullptr before freeing, there's a risk of undefined behavior if the member is accessed before being properly initialized.

-    SDL_Surface *mSurface;
+    SDL_Surface *mSurface = nullptr;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SDL_Surface *mSurface;
- SDL_Surface *mSurface;
+ SDL_Surface *mSurface = nullptr;


~SurfaceImpl() {
clear();
Expand All @@ -54,7 +53,6 @@ struct SurfaceImpl {
}
};

/// @brief Implementation of a SDL2 TTF font.
struct FontImpl {
TTF_Font *mFontImpl;

Expand Down
5 changes: 1 addition & 4 deletions src/tinyui.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
MIT License

Copyright (c) 2022-2025 Kim Kulling
Copyright (c) 2022-2024 Kim Kulling

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -57,7 +57,6 @@ void log_message(LogSeverity severity, const char *message) {

ret_code TinyUi::init(Context &ctx) {
if (ctx.mCreated) {
printf("Error: Context is already inited\n");
return ErrorCode;
}

Expand All @@ -78,7 +77,6 @@ ret_code TinyUi::initScreen(Context &ctx, int32_t x, int32_t y, int32_t w, int32
ret_code TinyUi::getSurfaceInfo(Context &ctx, int32_t &w, int32_t &h) {
w = h = -1;
if (!ctx.mCreated) {
printf("Error: Context is not inited\n");
return ErrorCode;
}

Expand Down Expand Up @@ -111,7 +109,6 @@ ret_code TinyUi::endRender(Context &ctx) {

ret_code TinyUi::release(Context &ctx) {
if (!ctx.mCreated) {
printf("Error: Context is not inited\n");
return ErrorCode;
}
Renderer::releaseRenderer(ctx);
Expand Down
15 changes: 12 additions & 3 deletions src/tinyui.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
MIT License

Copyright (c) 2022-2025 Kim Kulling
Copyright (c) 2022-2024 Kim Kulling

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -250,10 +250,17 @@ struct Events {
static constexpr int32_t MouseButtonUpEvent = 2;
static constexpr int32_t MouseMoveEvent = 3;
static constexpr int32_t MouseHoverEvent = 4;
static constexpr int32_t UpdateEvent = 5;
static constexpr int32_t KeyDownEvent = 5;
static constexpr int32_t KeyUpEvent = 6;
static constexpr int32_t UpdateEvent = 7;
static constexpr int32_t NumEvents = UpdateEvent + 1;
};

struct EventData {
uint8_t data[16] = {};

};

/// @brief This interface is used to store all neede message handlers.
struct CallbackI {
/// The function callback
Expand Down Expand Up @@ -326,6 +333,7 @@ struct Context {
SDLContext mSDLContext; ///< The SDL context.
Style mStyle; ///< The active style.
Widget *mRoot; ///< The root widget.
Widget *mFocus; ///< The widget which is in focus.
tui_log_func mLogger = nullptr; ///< The logger function.
EventDispatchMap mEventDispatchMap; ///< The event dispatch map.
FontCache mFontCache; ///< The font cache.
Expand All @@ -351,7 +359,8 @@ struct Context {
mWindowsTitle(nullptr),
mSDLContext(),
mStyle(),
mRoot(nullptr) {
mRoot(nullptr),
mFocus(nullptr) {
// empty
}
};
Expand Down
88 changes: 63 additions & 25 deletions src/widgets.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
MIT License

Copyright (c) 2022-2025 Kim Kulling
Copyright (c) 2022-2024 Kim Kulling

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -56,16 +56,19 @@ static Image *loadIntoImageCache(Context &ctx, const char *filename) {
return image;
}

int w = 0;
int h = 0;
int bytesPerPixel = 0;
image = new Image;
if (image == nullptr) {
return nullptr;
}

Comment on lines +59 to +63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Prevent memory-leak when stbi_load fails
image is allocated but never freed if stbi_load returns nullptr. This leaks every time an image cannot be loaded.

-    unsigned char *data = stbi_load(filename, &w, &h, &bytesPerPixel, 0);
-    if (data == nullptr) {
-        return nullptr;
+    unsigned char *data = stbi_load(filename, &w, &h, &bytesPerPixel, 0);
+    if (data == nullptr) {
+        delete image;                 // reclaim the allocation
+        return nullptr;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
image = new Image;
if (image == nullptr) {
return nullptr;
}
image = new Image;
if (image == nullptr) {
return nullptr;
}
unsigned char *data = stbi_load(filename, &w, &h, &bytesPerPixel, 0);
if (data == nullptr) {
delete image; // reclaim the allocation
return nullptr;
}

int w, h, bytesPerPixel;
unsigned char *data = stbi_load(filename, &w, &h, &bytesPerPixel, 0);
if (data == nullptr) {
return nullptr;
}
image = new Image;
int pitch = w * bytesPerPixel;


int32_t pitch = w * bytesPerPixel;
pitch = (pitch + 3) & ~3;
image->mSurfaceImpl = Renderer::createSurfaceImpl(data, w, h, bytesPerPixel, pitch);
image->mX = w;
Expand Down Expand Up @@ -110,8 +113,18 @@ static Widget *createWidget(Context &ctx, Id id, Id parentId, const Rect &rect,
return widget;
}

void eventDispatcher() {
void eventDispatcher(Context &ctx, int32_t eventId, EventData *eventData) {
if (ctx.mFocus == nullptr) {
return;
}

if (eventId == Events::KeyDownEvent || eventId == Events::KeyUpEvent) {
if (eventData != nullptr) {
if (ctx.mFocus->mType == WidgetType::TextField) {
ctx.mFocus->mText.append((const char*)eventData->data[0]);
}
}
}
}
Comment on lines +116 to 128
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Unsafe cast when appending a single key to mText
eventData->data[0] is a single byte, yet it is re-interpreted as a C-string:

ctx.mFocus->mText.append((const char*)eventData->data[0]);

This is undefined behaviour and usually crashes because the pointer is interpreted as an address. Append the character itself:

-                ctx.mFocus->mText.append((const char*)eventData->data[0]);
+                ctx.mFocus->mText.push_back(static_cast<char>(eventData->data[0]));

Additional thought: you probably want to filter non-printable characters and support UTF-8.


ret_code Widgets::container(Context &ctx, Id id, Id parentId, const char *text, const Rect &rect) {
Expand Down Expand Up @@ -190,6 +203,22 @@ ret_code Widgets::label(Context &ctx, Id id, Id parentId, const char *text, cons
return ResultOk;
}

ret_code Widgets::textfield(Context &ctx, Id id, Id parentId, const Rect &rect, Alignment alignment) {
if (ctx.mRoot == nullptr) {
return ErrorCode;
}

Widget *widget = createWidget(ctx, id, parentId, rect, WidgetType::TextField);
if (widget == nullptr) {
return ErrorCode;
}

widget->mAlignment = alignment;

return ResultOk;
}


ret_code Widgets::button(Context &ctx, Id id, Id parentId, const char *text, const char *image, const Rect &rect, CallbackI *callback) {
if (ctx.mSDLContext.mRenderer == nullptr) {
return ErrorCode;
Expand Down Expand Up @@ -227,20 +256,6 @@ ret_code Widgets::box(Context &ctx, Id id, Id parentId, const Rect &rect, bool f
return ResultOk;
}

ret_code Widgets::toolbar(Context &ctx, Id id, Id parentId, const Rect &rect) {
if (ctx.mSDLContext.mRenderer == nullptr) {
ctx.mLogger(LogSeverity::Error, "TUI-Renderer is nullptr.");
return ErrorCode;
}

Widget *child = createWidget(ctx, id, parentId, rect, WidgetType::ToolBar);
if (child == nullptr) {
return ErrorCode;
}

return ResultOk;
}

ret_code Widgets::panel(Context &ctx, Id id, Id parentId, const char *title, const Rect &rect, CallbackI *callback) {
if (ctx.mSDLContext.mRenderer == nullptr) {
ctx.mLogger(LogSeverity::Error, "TUI-Renderer is nullptr.");
Expand Down Expand Up @@ -302,7 +317,6 @@ ret_code Widgets::progressBar(Context &ctx, Id id, Id parentId, const Rect &rect
ctx.mUpdateCallbackList.push_back(callback);
}


return ResultOk;
}

Expand Down Expand Up @@ -360,7 +374,15 @@ static void render(Context &ctx, Widget *currentWidget) {
const uint32_t fillRate = state->filledState;
const uint32_t width = r.width * fillRate / 100;
Renderer::drawRect(ctx, r.x1, r.y1, width, r.height, true, ctx.mStyle.mTextColor);
} break;
}
break;

case WidgetType::TextField:
{
Renderer::drawRect(ctx, r.x1, r.y1, r.width, r.height, true, ctx.mStyle.mFg);
Renderer::drawRect(ctx, r.x1+2, r.y1+2, r.width-4, r.height-4, true, ctx.mStyle.mBorder);
}
break;
Comment on lines +380 to +385
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Render the text inside the TextField
The current rendering draws the box but never shows the user-typed text, making the widget appear empty.

         Renderer::drawRect(ctx, r.x1+2, r.y1+2, r.width-4, r.height-4, true, ctx.mStyle.mBorder);
+        if (!currentWidget->mText.empty()) {
+            Color4 fg = ctx.mStyle.mTextColor, bg = ctx.mStyle.mBg;
+            Renderer::drawText(ctx, currentWidget->mText.c_str(),
+                               ctx.mSDLContext.mDefaultFont,
+                               currentWidget->mRect, fg, bg,
+                               currentWidget->mAlignment);
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case WidgetType::TextField:
{
Renderer::drawRect(ctx, r.x1, r.y1, r.width, r.height, true, ctx.mStyle.mFg);
Renderer::drawRect(ctx, r.x1+2, r.y1+2, r.width-4, r.height-4, true, ctx.mStyle.mBorder);
}
break;
case WidgetType::TextField:
{
Renderer::drawRect(ctx, r.x1, r.y1, r.width, r.height, true, ctx.mStyle.mFg);
Renderer::drawRect(ctx, r.x1+2, r.y1+2, r.width-4, r.height-4, true, ctx.mStyle.mBorder);
if (!currentWidget->mText.empty()) {
Color4 fg = ctx.mStyle.mTextColor, bg = ctx.mStyle.mBg;
Renderer::drawText(ctx,
currentWidget->mText.c_str(),
ctx.mSDLContext.mDefaultFont,
currentWidget->mRect,
fg, bg,
currentWidget->mAlignment);
}
}
break;


case WidgetType::Container:
case WidgetType::Box:
Expand Down Expand Up @@ -428,7 +450,12 @@ void Widgets::onKey(Context &ctx, const char *key, bool isDown) {
return;
}

eventDispatcher();
if (isDown) {
EventData eventData;
eventData.data[0] = *key;
eventDispatcher(ctx, Events::KeyDownEvent, &eventData);

}
}

void recursiveClear(Widget *current) {
Expand Down Expand Up @@ -475,6 +502,17 @@ bool Widgets::isEnabled(Context &ctx, Id id) {
return false;
}

ret_code Widgets::setFocus(Context &ctx, Id id) {
Widget *widget = findWidget(id, ctx.mRoot);
if (widget == nullptr) {
return ErrorCode;
}

ctx.mFocus = widget;

return ResultOk;
}

Widget *Widgets::getWidgetById(Context &ctx, Id id) {
return findWidget(id, ctx.mRoot);
}
Expand Down
Loading