Skip to content

Add SDL3 #779

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open

Add SDL3 #779

wants to merge 21 commits into from

Conversation

RobLoach
Copy link
Contributor

@RobLoach RobLoach commented Feb 20, 2025

@zoogies I've removed the sdl_gpu stuff, since it wasn't there yet, and cleaned up the CMake a bit. Mind a review?

@zoogies
Copy link

zoogies commented Feb 20, 2025

Sure! I'll take a look in a few hours. Thanks

@RobLoach
Copy link
Contributor Author

Also made it thread-safe. There are a couple things that I think could improve. I'm not 100% sure it's cleaning up the memory correctly tho

char *str = 0;
(void)usr;
if (!len) return;
str = (char*)SDL_malloc((size_t)len+1);

Choose a reason for hiding this comment

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

In case of multi-byte text, like SDL TextInput which is unicode, len is count grapheme clusters or glyphs, not bytes.

return SDL_Fail();
}

SDL_StartTextInput(appContext->window);

Choose a reason for hiding this comment

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

On desktop platforms, SDL_StartTextInput() is implicitly called on SDL window creation. On some, typically non desktop, platforms this call implicitly enables and shows virtual keyboard.

Copy link

Choose a reason for hiding this comment

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

In SDL3, this needs to be explicitly called on Desktop platforms. I think adding a comment about the behavior of calling this on mobile is probably in order

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, is there a cleaner way we could accomplish this? Perhaps a callback when a textbox on focus/blur?


case SDL_EVENT_MOUSE_MOTION:
if (ctx->input.mouse.grabbed) {
int x = (int)ctx->input.mouse.prev.x, y = (int)ctx->input.mouse.prev.y;

Choose a reason for hiding this comment

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

SDL3 (mouse) movement events were changed from integer to float. Nuklear uses float internally too. Is the integer cast truncation intentional?

Copy link

Choose a reason for hiding this comment

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

This was a hack on my part, changing to float and removing the cast should be fine

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Know the significance of ctx->input.mouse.grabbed? Looking at the other implementations, I'm not quite sure how they'd apply here.

Choose a reason for hiding this comment

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

It seems as if while window is moved around by grabbing it with mouse, then absolute mouse pointer x,y cannot be trusted and thus relative x,y are being used.

The SDL documentation says in this scenario SDL_GetGlobalMouseState() is supposed to be used.

But actually the situation is more complex. Typically a full screen or borderless window SDL application would force relative mouse movement, and locked mouse into window by SDL_SetRelativeMouseMode. This mode grabs the mouse pointer by the window and hides the system cursor or something like that. And when the application switches to windowed mode, then relative mouse mode is disabled and mouse is only locked otionally into window when window gets focus and mouse gets unlocked when window loses focus.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After some testing, I think what I've put in there seems to be working with or without window grabbing.

case SDL_EVENT_MOUSE_MOTION:
    ctx->input.mouse.prev = ctx->input.mouse.pos;
    ctx->input.mouse.delta.x = evt->motion.xrel;
    ctx->input.mouse.delta.y = evt->motion.yrel;
    ctx->input.mouse.pos.x = evt->motion.x;
    ctx->input.mouse.pos.y = evt->motion.y;
    return 1;

I played around with SetRelativeMouseMode too, and it didn't seem to have any real affect...

case SDL_EVENT_MOUSE_MOTION:
{
    struct nk_sdl* sdl = (struct nk_sdl*)ctx->userdata.ptr;
    ctx->input.mouse.prev = ctx->input.mouse.pos;
    ctx->input.mouse.delta.x = evt->motion.xrel;
    ctx->input.mouse.delta.y = evt->motion.yrel;
    ctx->input.mouse.pos.x = evt->motion.x;
    ctx->input.mouse.pos.y = evt->motion.y;
    if (ctx->input.mouse.grabbed) {
        SDL_SetWindowRelativeMouseMode(sdl->win, true);
        ctx->input.mouse.pos.x += evt->motion.x;
        ctx->input.mouse.pos.y += evt->motion.y;
    }
    else {
        SDL_SetWindowRelativeMouseMode(sdl->win, false);
        ctx->input.mouse.pos.x = evt->motion.x;
        ctx->input.mouse.pos.y = evt->motion.y;
    }
    return 1;
}

Overall, I think what's in there is fine and don't want to over-complicate it 🤷 ... What do you think?

Choose a reason for hiding this comment

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

I agree, it is fine as it is.

SDL_SetWindowRelativeMouseMode() is best to use when the application is in full screen or borderless full screen modes. The mouse cursor is confined into the whole screen (so app window), the OS / HW mouse pointer is hidden (but software cursor could be used), and mouse motion events are rapidly reporting mouse motion position and delta position. This is fit for most action games where you control a player character. Or in case of emulators where cursor is also emulated, e.g. DOSBox.

The way you used that API is somewhat wrong as when relative mode is enabled, the current event does not reflect the changes yet. In relative mode we would only use evt->motion.xrel/yrel. In normal mode we would use only the absolute position so evt->motion.x/y.

case SDL_EVENT_TEXT_INPUT:
{
nk_glyph glyph;
SDL_memcpy(glyph, evt->text.text, NK_UTF_SIZE);
Copy link

@klei1984 klei1984 Feb 21, 2025

Choose a reason for hiding this comment

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

evt->text.text is UTF-8 encoded const char* nul terminated string. Based on limited research it is not guaranteed that a single glyph is reported by this event. Could it happen that text input gets cropped as the UTF-8 input is not passed per glyph to nk_input _glyph in a loop?

@zoogies
Copy link

zoogies commented Feb 21, 2025

I don't have push access to this branch, but this patch fixes the casting klei mentioned:

index 5a8c589..e819169 100644
--- a/demo/sdl3/nuklear_sdl3_renderer.h
+++ b/demo/sdl3/nuklear_sdl3_renderer.h
@@ -308,10 +308,10 @@ nk_sdl_handle_event(struct nk_context* ctx, SDL_Event *evt)
 
         case SDL_EVENT_MOUSE_MOTION:
             if (ctx->input.mouse.grabbed) {
-                int x = (int)ctx->input.mouse.prev.x, y = (int)ctx->input.mouse.prev.y;
-                nk_input_motion(ctx, x + evt->motion.xrel, y + evt->motion.yrel);
+                float x = ctx->input.mouse.prev.x, y = ctx->input.mouse.prev.y;
+                nk_input_motion(ctx, (int)(x + evt->motion.xrel), (int)(y + evt->motion.yrel));
             }
-            else nk_input_motion(ctx, evt->motion.x, evt->motion.y);
+            else nk_input_motion(ctx, (int)evt->motion.x, (int)evt->motion.y);
             return 1;
 
         case SDL_EVENT_TEXT_INPUT:
@@ -323,7 +323,7 @@ nk_sdl_handle_event(struct nk_context* ctx, SDL_Event *evt)
             return 1;
 
         case SDL_EVENT_MOUSE_WHEEL:
-            nk_input_scroll(ctx,nk_vec2((float)evt->wheel.x,(float)evt->wheel.y));
+            nk_input_scroll(ctx, nk_vec2(evt->wheel.x, evt->wheel.y));
             return 1;
     }
     return 0;

The first casting change is technically unnecessary, but it allows adding the prev and rel values as floats before converting them back to ints.

Copy link
Contributor Author

@RobLoach RobLoach left a comment

Choose a reason for hiding this comment

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

Added a few more changes.


case SDL_EVENT_MOUSE_MOTION:
if (ctx->input.mouse.grabbed) {
int x = (int)ctx->input.mouse.prev.x, y = (int)ctx->input.mouse.prev.y;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

After some testing, I think what I've put in there seems to be working with or without window grabbing.

case SDL_EVENT_MOUSE_MOTION:
    ctx->input.mouse.prev = ctx->input.mouse.pos;
    ctx->input.mouse.delta.x = evt->motion.xrel;
    ctx->input.mouse.delta.y = evt->motion.yrel;
    ctx->input.mouse.pos.x = evt->motion.x;
    ctx->input.mouse.pos.y = evt->motion.y;
    return 1;

I played around with SetRelativeMouseMode too, and it didn't seem to have any real affect...

case SDL_EVENT_MOUSE_MOTION:
{
    struct nk_sdl* sdl = (struct nk_sdl*)ctx->userdata.ptr;
    ctx->input.mouse.prev = ctx->input.mouse.pos;
    ctx->input.mouse.delta.x = evt->motion.xrel;
    ctx->input.mouse.delta.y = evt->motion.yrel;
    ctx->input.mouse.pos.x = evt->motion.x;
    ctx->input.mouse.pos.y = evt->motion.y;
    if (ctx->input.mouse.grabbed) {
        SDL_SetWindowRelativeMouseMode(sdl->win, true);
        ctx->input.mouse.pos.x += evt->motion.x;
        ctx->input.mouse.pos.y += evt->motion.y;
    }
    else {
        SDL_SetWindowRelativeMouseMode(sdl->win, false);
        ctx->input.mouse.pos.x = evt->motion.x;
        ctx->input.mouse.pos.y = evt->motion.y;
    }
    return 1;
}

Overall, I think what's in there is fine and don't want to over-complicate it 🤷 ... What do you think?

return SDL_Fail();
}

SDL_StartTextInput(appContext->window);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, is there a cleaner way we could accomplish this? Perhaps a callback when a textbox on focus/blur?

Comment on lines +73 to +75
struct nk_sdl* sdl;
NK_ASSERT(ctx);
sdl = (struct nk_sdl*)ctx->userdata.ptr;
Copy link
Contributor Author

@RobLoach RobLoach Feb 22, 2025

Choose a reason for hiding this comment

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

One worry I have about this strategy is that the SDL renderer is taking over the nk_context userdata to store information required by the renderer. If the application is making use of the userdata, there will be a conflicts unless they change to use nk_sdl_set_userdata() instead.

Perhaps we introduce another renderer-specific userdata property? Something like void* renderer_data or something? As an added benefit, that could be used in some of the other renderers too (glfw_opengl2, sfml_gl3, gdi, etc).

Choose a reason for hiding this comment

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

I assume the renderer backends are still examples only and not part of the UI library, which is a single header. E.g. I change nk_sdl_handle_event to add more keys, etc. As integrator, I am in full control over the userdata. E.g. I do not have nk_sdl sdl at all, I embed the stuff from it into my own context thingy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So you use your own userdata elsewhere outside of the nuklear context?

Copy link

Choose a reason for hiding this comment

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

Um, I do not use nuklear_sdl3_renderer.h and APIs like nk_sdl_render(struct nk_context* ctx at all. I use the demo as an example and adapt it to my needs. E.g. my own version of nk_sdl_render() gets not a Nuklear context, it gets an application context that embeds the Nuklear context too. What I wanted to communicate above is that the life cycle of the Nuklear context is shorter then the application context in my case, and that I consider nuklear_sdl3_renderer.h to be an example, not the official API.

So the answer to your question is actually no. I do not have a specific need for userdata enabled by NK_INCLUDE_COMMAND_USERDATA, it can be used by nuklear_sdl3_renderer.h and I will adapt that header content as I need.

  • Text processing: In case of text processing I use freetype2, different texture packer, custom font atlas, and custom cached glyph encoding library that are not exclusive to nk_user_font, and Nuklear. So the life cycle of my text rendering sub system is typically longer than the life cycle of the Nuklear context. The font atlas is based on SDL_surface objects where I could do palette color manipulations, which is not a typical use case I admit, and when the Nuklear context is created and render backend is selected the font atlas layers are converted to SDL_textrure objects in native texture format. I actually intend to use SDL3 GPU API not the SDL3 renderer. That is important as my whole design revolves around SDL3 GPU's ability to transparently select any backend that is available. I am not deciding which backend to use so my font atlas needs to remain backend agnostic (e.g. uses SDL_surface not textures that have backend dependent pixel formats).

  • GUI: my application context is not a sub feature of Nuklear, instead Nuklear's context is the sub feature of my application context. The same way as for text processing, I tried to shoot for a design which separates the GUI library and its render backend and the application core logic even if this is very difficult with the immediate mode pattern. Nuklear requires a lot of boilerplate and that boilerplate is interleaved into the core application logic which is difficult to maintain so I try to encapsulate, structure and generalize things for which I created an abstraction level above Nuklear similar to a c++ object oriented wrapper would do, but focusing on features (buttons, lists, hotkeys, sound effects, ...) instead of API coverage. I use a custom GUI layout manager that is loading parameters and asset or resource IDs from json files above and independent of Nuklear. Assets are in custom file formats using LZ4 compression and are loaded into renderer agnostic SDL_surface objects. Later on when the system specific render backend is known, D3D/OGL/etc, then these surfaces are converted and loaded into GPU memory into SDL_texture objects, using native texture formats, with assets packed as most optimal for particular screen content expected to be rendered. I only use Nuklear STATIC layout and position every top level (custom) widgets inside the application frontend or menu screens. Nuklear's render target is just an SDL_texture not the swapchain texture or whatever. In short my application's context is one order or abstraction level higher than Nuklear's context. The Nuklear context is only created after the render backend is determined. I am sure there is a better way to do all of this, but this is what i came up with for my particular needs.

@zoogies
Copy link

zoogies commented Mar 30, 2025

What is the state of this PR? I'm happy to help with any remaining items

@RobLoach
Copy link
Contributor Author

I believe all that's really missing is...

  • Testing
  • Figure out the best method around SDL_StartTextInput() usage
  • UTF-8 support in SDL_EVENT_TEXT_INPUT

@zoogies
Copy link

zoogies commented Mar 31, 2025

Cool, for SDL_StartTextInput(), running this once on init should be okay unless we are on a non-desktop platform.

Sam told me in the SDL Discord to "Remember [that] this will bring up the on-screen keyboard on some platforms, so you should only do this when you're ready for text input".

We could macro guard it or put a comment above it so people are aware of its behavior. I treat the source files for the backends as examples, but the header files for the backends as Nuklear vendored, so IMO it's not unreasonable to just put a disclaimer since it's "just an example".

Alternatively, you could place the start and stop calls inside the focus/defocus events for text inputs or windows, but that might interfere with some edge cases.

ref: https://wiki.libsdl.org/SDL3/SDL_StartTextInput

@zoogies
Copy link

zoogies commented Mar 31, 2025

Actually, Sam also said that:

Conceptually it should be tied to an input box getting focus.

So it sounds like the latter is the best option. Any ideas on where we should hook into the input focus? I'm not too familiar on whether that should lie at a window focus or input focus boundary. Are there any cases of keyboard navigation that would make sense on desktop but not on other platforms?

@zoogies
Copy link

zoogies commented May 7, 2025

I've fully integrated this into my own project, and it's working nicely so far except for one minor issue.

This is observable in demo/sdl3, but repeated clicking (a checkbox or other input) only registers the first click, and then drops subsequent clicks until a set amount of time has elapsed without a click. ie: spam clicking only registers the first click

Also, I started looking into the SDL TextInput stuff and it looks like the most trivial solution would be to somehow insert the calls into nk_edit_focus and nk_edit_unfocus. There doesn't seem to be a great way to do that exclusively from the SDL3 backend though. @RobLoach, do you have any guidance on a possible refactor to make this doable?

@RobLoach
Copy link
Contributor Author

RobLoach commented May 8, 2025

looking into the SDL TextInput stuff and it looks like the most trivial solution would be to somehow insert the calls into nk_edit_focus and nk_edit_unfocus.

Agreed. Not an elegant solution currently. Are you proposing some kind of event callback system for them?

I've fully integrated this into my own project

The other thing I'd like to see incorporated into this is a clearer implementation of detailing with the global object state. Or something like encorporating #799 into here.

@zoogies
Copy link

zoogies commented May 8, 2025

Are you proposing some kind of event callback system for them?

That's the first thing that comes to mind. I don't have deep knowledge of the Nuklear codebase, so I wasn't sure if that's done in other places, or what the best way to integrate something like that would be. Do you have any opinions?

The other thing I'd like to see incorporated into this is a clearer implementation of detailing with the global object state. Or something like encorporating #799 into here.

This is interesting. Personally, I'm fine with the global state for my purposes. Does it make more sense to integrate these changes here, or have #799 tackle it for all of the SDL demos, once we have nailed down the API?

but repeated clicking (a checkbox or other input) only registers the first click

Did you have any thoughts on why this may be?

@rswinkle
Copy link
Contributor

Are you proposing some kind of event callback system for them?

That's the first thing that comes to mind. I don't have deep knowledge of the Nuklear codebase, so I wasn't sure if that's done in other places, or what the best way to integrate something like that would be. Do you have any opinions?

Maybe a strict callback isn't necessary as that would incur a small but present overhead for all backends. I could see it being 2 (mutually inclusive) optional macros that are called if they exist at appropriate places in Nuklear. It wouldn't just be nk_edit_focus/nk_edit_unfocus, because that's just for the manually forcing focus/unfocus, not from the user actually clicking on an edit field or something else. I'm not sure what the best name is but something like NK_ON_EDIT_FOCUS()/UNFOCUS() might work. Not sure about any necessary parameters either.

Just my two cents till I actually have the chance to review/test this and have a more informed opinion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants