DirectGUI adjusts to the window--but only within bounds

I’m currently looking into making the UI of a project work within a variety of aspect-ratios. I see that DirectGUI appears to implement automatic scaling of GUI items as the window-size changes, which would seem to do what I want. However, it also seems that DirectGUI’s approach to doing so assumes a square GUI, with a frame-size of (-1, 1, -1, 1).

See for example the following gifs, which compares the handling of two DirectFrames as the window-size is adjusted.

In the first gif, the DirectFrame has a frame-size of (-1, 1, -1, 1), and thus is adjusted nicely to the varying window-size.
directGUIAdjustment1

In the second, however, the DirectFrame has a frame-size of (-1.5, 1.5, -1, 1)–but is nevertheless still only adjusted when the window passes the (-1, 1, -1, 1) bounds. This results in part of the DirectFrame being cut off at certain aspect ratios.
directGUIAdjustment2

So, what does one do about this? Can the threshold for this automatic adjustment be changed? Might said adjustment be disabled, and the matter handled via custom code? Would it be better to leave DirectGUI’s behaviour as-is, and to try to add custom-code that just handles the edge-cases? Or something else besides…?

This is due to how aspect ratio is handled for the aspect2d node in Panda3D.
You can add custom code to catch window events and recalculate the UI according to the new aspect.

Alternatively you can take a look at my layout widgets here DirectGuiExtension · PyPI which may help you scale and position your widgets as you want and also the DirectAutoSizer will handle the window size changes automatically if enabled.

1 Like

Hmm… But will such custom code and DirectGUI’s built-in code not interfere with each other…? (I’m guessing that ShowBase does more with its window-events than just this, and thus I imagine that I do want to pass the window-event on after using it.)

Hah, it is tempting! But right now I have (at least a large chunk of) my UI pretty much made, I do think–I’m very hesitant to re-make everything from scratch!

If you don’t use showbases’ base to lead the event to another method than the bases window event handler function you should be fine otherwise just call the method from base either before or after your custom code.

oh and also you don’t really need to redo your whole GUI when using the extension library, just parent the base elements to the sizer widgets should be enough. If you have specific functions to recalculate the size and position of some GUI elements, you can also let the sizer call it when it resizes.

I’m… not entirely sure that I follow.

So, let me instead show you what I’m doing. As it stands, DirectGUI’s auto-resizing code does seem to run.

# In my initialisation code:
        self.windowDirectObject = DirectObject()
        self.windowDirectObject.accept("window-event", self.windowUpdated)

# Elsewhere:
    def windowUpdated(self, graphicsWindow):
        ShowBase.windowEvent(self, graphicsWindow)

        # Adjustment of the UI would presumably go here

        # This is gameplay-related--or more accurately, related
        # to my approach to rendering certain backgrounds
        if self.currentWorld is not None:
            self.currentWorld.updateBacking()

Ah, I see–that is tempting, then.

Still, since this seems like a fairly minor matter, I think that I’d like to simply attend to it within my own code. And indeed, to not bring in a third-party module just for this one purpose, for that matter.

The way you set up your window-event handling looks correct, just, that you may not even need the ShowBase.windowEvent call since that’s already caught in ShowBase itself and as you catch the event with your windowDirectObject, you won’t override the “accept” call done from within ShowBase.

If you want to make sure the code in ShowBases’ window event is called before your code, then you can do so but you may want to ignore the window-event in ShowBase by doing base.ignore(“window-event”) as otherwise it probably will get called twice.

Ah, hmm, I hadn’t thought of that, I’ll confess! That’s a good point–I should perhaps remove the call within my event and just let ShowBase handle the event as it receives it.

But that still doesn’t answer my concern (I think): Since the automatic adjustment of DirectGUI seems to be active with the above code, would adding my own code cause the two pieces of logic to interfere with each other…?

That depends on your way of setting up the GUI. The aspect2d node which DirectGUI parents to by default, will always scale your GUI elements accordingly but it won’t change their aspect to fit the windows aspect. Changing the aspect of the GUI is up to you so for example, if you set up a frame with size (1*aspect, 1, 1) you need to ensure the aspect gets updated whenever the windows aspect changes.

In my GUI if such feature is required I have all the placement and sizing code moved to a function which I can run on every window-event call.

A simple example may look like the following (took me a while because of the window sizes where x < y):

from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectButton import DirectButton

app = ShowBase()

btn = DirectButton(text="test", frameSize=(-1,1,-1,1))

def updateButton(win):
    app.windowEvent(win)

    #print(app.getAspectRatio())

    if app.getAspectRatio() >= 1:
        btn["frameSize"] = (-1 * app.getAspectRatio(), 1 * app.getAspectRatio(), -1, 1)
    else:
        y_ratio = base.win.get_size().y / base.win.get_size().x
        btn["frameSize"] = (-1, 1, -1*y_ratio, 1*y_ratio)

app.accept("window-event", updateButton)

app.run()

Ah, I should perhaps clarify: I don’t want to change the aspect ratio of the UI. Rather, I want to change the scale of the UI such that it fits into the new aspect ratio of the window (if called for).

You can see this happening automatically in the gifs above: note that the aspect ratios of the DirectFrames used there don’t change, but that the scales of those frames do.

Hm, I’m not sure I understand correct. Maybe you can describe with the gives you posted how you expect the Frames to actually behave?

For example if the window shrinks but still has a landscape aspect, how should the frame be re-sized? Currently it shrinks only if the aspect is less then 1. Do you want it to shrink to always fit into the window but keeps the aspect it had previously?

In short, in the second gif I would want the frame to start shrinking earlier–specifically, as soon as the window becomes too narrow for it to fit. Such that the frame is always entirely visible, never cut off.

Pretty much so, yes.

Ah ok, then there’s a bit more calculating involved. You need to check if the edges of the gui element are within the windows borders which are basically left/right/bottom/top -1*aspect / 1*aspect / -1 / 1 for a normal landscape sized window or with the y-aspect multiplied with the bottom and top. Then if the element is too large, calculate the difference and shrink it by that amount.

That’s fair, and thank you.

I am still concerned about such code and the built-in handling both being active, and potentially causing issues as a result. Can the built-in handling be disabled?

The only thing the built-in handling does which is causing the UI scaling is updating the scale of aspect2d.
As from the showBase code:

... in adjustWindowAspectRatio ...

# If the window is TALL, lets expand the top and bottom
self.aspect2d.setScale(1.0, aspectRatio, aspectRatio)

...

# If the window is WIDE, lets expand the left and right
self.aspect2d.setScale(1.0 / aspectRatio, 1.0, 1.0)

There’s also quite a few other things happening but this is probably the only thing you need to consider when updating your UI elements scales.

Ah, I see–that is encouraging then. Thank you for your help! :slight_smile: