<![CDATA[Carrot Games]]> http://community.carrot-games.com Smartfeed extension for phpBB <![CDATA[Iris Blade :: Scrollbars! :: Author celes]]> 2026-03-18T19:27:43+00:00 2026-03-18T19:27:43+00:00 http://community.carrot-games.com/viewtopic.php?f=7&t=33&p=86#p86
We take so much for granted from our UIs... I didn't even realize I'd need scrollbars! I keep going back and forth thinking whether rolling my own UI system was a good idea, but seeing how I was able to a add scrollbar container in about an hour, I'm on team "good idea" today! :akko_smile:
scrollbars.gif
The biggest ux hurdle here was making sure this is both keyboard navigable and mouse navigable and to make no compromises on either of the navigation modes. For mouse mode, that means being able to use the scrollbar and hovering widgets freely, and for keyboard it means the scrollbar should follow the focused element so that you always see what you're selecting.

Coming up with a system to handle both was easy once I gave this some proper thought and made everything click. My UI interaction "manager" thing already keeps track of the current interaction mode, so we know whether we're on key mode (keyboard or gamepad) or pointer (i.e. mouse) mode. There are some actions that let you switch between the two.

The interesting challenge was in keyboard mode, where I had to detect if the currently focused element changed. I added a "justFocused " property just for that which gives me the UiRect and sense id for the widget that was focused. If the sense id for the widget is one that is inside the scrollbar container and the rectangle is inside thethe visible range, we adjust the scroll position doing some math. One thing I kept struggling with is how do I know what widgets are inside the scrollbar container. That is achieved with code that looks like this inside the scrollbar's draw code:

Code: Select all

uiInteraction.BeginRecording("some-unique-key");
foreach (var child in children) {
    child.Draw(...);
}
var childInScrollbar = uiInteraction.EndRecording("some-unique-key");
Another detail worth mentioning is how I didn't do a modular "ScrollBar" node that you can put anything under but instead a "ScrollbarColumn", which acts as a VBox *with* a scrollbar. I do that because when adjusting the offset, I make sure to snap to one of the rows. This is very situational but I find it improves usability when you have a row with lots of tiny widgets, since otherwise you might end up scrolling halfway through the section to make one of the child widgets visible.

One day I should write about my UI system in a more comprehensive way. Not sure if that made any sense :nkoThink:

Anyway that's all for today!

Attachments


scrollbars.gif (1189.88 KiB)

]]>

We take so much for granted from our UIs... I didn't even realize I'd need scrollbars! I keep going back and forth thinking whether rolling my own UI system was a good idea, but seeing how I was able to a add scrollbar container in about an hour, I'm on team "good idea" today! :akko_smile:
scrollbars.gif
The biggest ux hurdle here was making sure this is both keyboard navigable and mouse navigable and to make no compromises on either of the navigation modes. For mouse mode, that means being able to use the scrollbar and hovering widgets freely, and for keyboard it means the scrollbar should follow the focused element so that you always see what you're selecting.

Coming up with a system to handle both was easy once I gave this some proper thought and made everything click. My UI interaction "manager" thing already keeps track of the current interaction mode, so we know whether we're on key mode (keyboard or gamepad) or pointer (i.e. mouse) mode. There are some actions that let you switch between the two.

The interesting challenge was in keyboard mode, where I had to detect if the currently focused element changed. I added a "justFocused " property just for that which gives me the UiRect and sense id for the widget that was focused. If the sense id for the widget is one that is inside the scrollbar container and the rectangle is inside thethe visible range, we adjust the scroll position doing some math. One thing I kept struggling with is how do I know what widgets are inside the scrollbar container. That is achieved with code that looks like this inside the scrollbar's draw code:

Code: Select all

uiInteraction.BeginRecording("some-unique-key");
foreach (var child in children) {
    child.Draw(...);
}
var childInScrollbar = uiInteraction.EndRecording("some-unique-key");
Another detail worth mentioning is how I didn't do a modular "ScrollBar" node that you can put anything under but instead a "ScrollbarColumn", which acts as a VBox *with* a scrollbar. I do that because when adjusting the offset, I make sure to snap to one of the rows. This is very situational but I find it improves usability when you have a row with lots of tiny widgets, since otherwise you might end up scrolling halfway through the section to make one of the child widgets visible.

One day I should write about my UI system in a more comprehensive way. Not sure if that made any sense :nkoThink:

Anyway that's all for today!

Attachments


scrollbars.gif (1189.88 KiB)

]]>
<![CDATA[Iris Blade :: Re: Scrollbars! :: Reply by Sugui]]> 2026-03-18T21:23:07+00:00 2026-03-18T21:23:07+00:00 http://community.carrot-games.com/viewtopic.php?f=7&t=33&p=87#p87 I liked a lot what you did to record the buttons that are actually in the scrolbar!

So I guess that the .Draw() function adds them to the current recording, if there is one. But do elements call .Draw() also when they aren't visible on the screen? Because it can be elements not visible due to the scrollbar progress, but its .Draw() function being called anyways :nkoThink:]]>
I liked a lot what you did to record the buttons that are actually in the scrolbar!

So I guess that the .Draw() function adds them to the current recording, if there is one. But do elements call .Draw() also when they aren't visible on the screen? Because it can be elements not visible due to the scrollbar progress, but its .Draw() function being called anyways :nkoThink:]]>
<![CDATA[Iris Blade :: Re: Scrollbars! :: Reply by celes]]> 2026-03-19T10:29:39+00:00 2026-03-19T10:29:39+00:00 http://community.carrot-games.com/viewtopic.php?f=7&t=33&p=88#p88
Sugui wrote: Wed Mar 18, 2026 9:23 pm So I guess that the .Draw() function adds them to the current recording, if there is one. But do elements call .Draw() also when they aren't visible on the screen? Because it can be elements not visible due to the scrollbar progress, but its .Draw() function being called anyways
I should take the time to properly blog about this at some point, but the way UI interaction works here is inspired by how dear imgui (and other similar imgui libraries) do it. During its draw method, widgets "sense" a portion of the screen. This call to .Sense() returns information about UI interaction on that location of the screen for the previous frame[1], so whether the mouse was hovering, clicking or dragging over that screen portion (a rectangle). In my case it's not just the mouse, but also whether key focus was on that widget, and whether the accept button was pressed over that widget.

So what the recording functionality does is registering all widgets that have called Sense() between the start and the end, and with that, you know all the things that are actually interested in user interaction.

[1] This frame of lag is part of what makes the whole thing work in immediate mode. When you draw a button, you immediately know if it was pressed during the previous frame (or rather, at the start of the current frame), and this enables the common immediate-mode pattern of "if draw_button() { /* do things ... */ }" even though in my framework it looks slightly different.]]>
Sugui wrote: Wed Mar 18, 2026 9:23 pm So I guess that the .Draw() function adds them to the current recording, if there is one. But do elements call .Draw() also when they aren't visible on the screen? Because it can be elements not visible due to the scrollbar progress, but its .Draw() function being called anyways
I should take the time to properly blog about this at some point, but the way UI interaction works here is inspired by how dear imgui (and other similar imgui libraries) do it. During its draw method, widgets "sense" a portion of the screen. This call to .Sense() returns information about UI interaction on that location of the screen for the previous frame[1], so whether the mouse was hovering, clicking or dragging over that screen portion (a rectangle). In my case it's not just the mouse, but also whether key focus was on that widget, and whether the accept button was pressed over that widget.

So what the recording functionality does is registering all widgets that have called Sense() between the start and the end, and with that, you know all the things that are actually interested in user interaction.

[1] This frame of lag is part of what makes the whole thing work in immediate mode. When you draw a button, you immediately know if it was pressed during the previous frame (or rather, at the start of the current frame), and this enables the common immediate-mode pattern of "if draw_button() { /* do things ... */ }" even though in my framework it looks slightly different.]]>