DirectGui layout system

Hi guys!

For those of you who have been looking for an automatic layout system to manage DirectGui widgets, I might have some good news :slight_smile: !

As I already implemented such a system for my own Panda3D Studio project, I decided to try and adapt it to work with DirectGui, and the results seem quite promising!

This should be considered a work in progress, as I haven’t fully tested things like DirectScrolledFrame management or adding/removing/resizing widgets at runtime, so certain functionality might still be missing while there could also be some redundant left-over code in there that needs to be cleaned up.

If you want to know what all the fuss is about, head on over to the Git repository and check it out.

In a nutshell, with this system you won’t ever have to worry about widget placement ever again, ever. All you do is define how a widget should be placed on the screen (left-aligned, centered, etc.) and how much space should surround it (left, right, bottom and top borders), as well as how it can stretch (horizontally, vertically, both or not at all). And that’s basically it; no need for exact positioning or sizing, as this will be done automatically for you, each time the window size changes.

This is accomplished by using “sizers” (people who used wxPython before will know what I’m talking about). You fill these abstract containers with objects in a certain direction, either horizontally or vertically.
(There is a GridSizer too, combining both horizontal and vertical sizers, but I haven’t ported it yet.)
These objects can not only be widgets, but also sizes (empty space) or even other sizers, so you can nest them as deeply as you like.
As you add each object to a sizer along its own direction, you can decide whether this object should fill out the entire available space in the other direction (the “expand” keyword argument is used for this) or whether it should be aligned to one side or centered (relative to this particular sizer, not to the entire screen).
If you need the object to stretch along the sizer’s direction itself, you can work with proportions.
For example, if you want to center a widget horizontally in a horizontal sizer, first add a (0, 0) size with a proportion of 1., then add the widget without a proportion, finally add another (0, 0) size with a proportion of 1. and Bob’s your uncle (or aunt, you never know these days).

Once you understand the subtleties of this approach, you will likely find it far more intuitive than the tedious trial and error routine of specifying position and size manually, only to find that resizing the window messes up your layout anyway.

If you’re interested in this project, please do let me know, otherwise I’m not sure how motivated I’ll be to keep working on it, as I’m not really into DirectGui anymore.
Currently this is just a little experiment, but if there’s enough interest, it might become something really useful.

To those who have a solid understanding of DirectGui (specifically those whose username starts with “Thauma” or ends with “db” :wink: ) I have a question (or two):

  1. What is the best/easiest way to know the exact size of any DirectGui widget? I’ve tried the getBounds method which works well, but only after at least one frame is rendered, which forced me to use some ugly workaround to get the correct values after creating the widgets.
  2. When I align the text of a DirectGui widget (e.g. with TextNode.A_center), resizing the widget afterwards using widget["frameSize"] doesn’t retain the alignment, i.e. the text just stays where it was. Is there some way to force an update of the text position on the widget?
3 Likes

You’re my saviour ^^ I was about to create my own layout manager, you saved my sanity (I already made a crappy one for DirectLabel only and DirectGui is not really user friendly to do that kind of stuff)

For DirectLabel I had troubles too, actually getBounds() return the object size once it is parented to another node which is already present in the scene (so aspect2d or any node already attached to it)

I will definitively use it :slight_smile: Well, once you’ve chosen a license :wink:

This is an interesting project and I’ve already thought about implementing something like that by my own and have already started with a small flow layout.
If your layout system would’ve been coded a bit more like standard DirectGui widgets, I could even imagine adding it as a custom feature to my DirectGui Designer.

Now to your questions.

Maybe calling resetFrameSize before getting the bounds could help. It’s a function of the base class of every DirectGui widget.
Other than that, there are probably no other methods to get the real size except if you already have set a specific FrameSize. As a side note, there are convenient functions (which also use bounds though) to get the width, height and center of a widget which are called getWidth, getHeight and getCenter.

Most DGui widgets inherit DirectFrame which most of the time is responsible for the text and background frame of the widget. The actual text then is a OnscreenText and has a separate background from the actual Frames background, which should actually be changed when the alignment changes. Now, resizing the Frame won’t update the Text as those are both separate elements. You need to tell the text that the frame has updated as otherwise the texts’ position won’t automatically be changed. I’m not entirely sure which function was responsible for resetting the text position, but you could try setText or also resetFrameSize.

Oh wow–I actually started in on something very like this, but abandoned it. It looks like you took it much further than I ever did! Very well done! :smiley:

As to your questions:

Hmm… I’m not sure. Aside from what others have already said, I have an arcane method of my own design that does this job. (I forget what prompted me to make it.) I could share it, if you like. :slight_smile:

However, could you give me a short bit of example-code showing this issue with “getBounds”? A quick experiment with a DirectButton had it reporting bounds immediately after the button was constructed, and while I don’t know whether those bounds are correct, they seem reasonable.

I think that there’s a caveat here: changing the frame-size doesn’t actually affect where the button thinks its “left”, “right”, and “centre” are. Instead, it’s the other way around: the frame-size seems to be specified relative to the text-anchor.

You can see this in action if you construct buttons with left- or centre- anchors, then change their frame-sizes such that they have a left-extent of zero. (Which should thus tell you where the buttons think that zero is.) You should find that in buttons with a left text-alignment, the left-hand extent of the button ends up at the left of the text, and that in buttons with a centre-alignment, the left-hand extent ends up at the centre of the text.

It’s arguably not intuitive, but it does seem to be the way that it works. ^^;

If I’m not much mistaken, “resetFrameSize” is used when doing the opposite: asking DirectGUI to calculate a new frame-size for some change, like new button-text.

Thanks all for your feedback, guys :slight_smile: !

Hmmm, I just tried that, but e.g. detaching the widget and reparenting it to pixel2d doesn’t make a difference.

It has a license now, so you should be able to sleep better when using my code for your nefarious purposes :stuck_out_tongue: .

Could you give me some examples of what exactly should be changed and how? It would certainly be cool to have an editor to create layouts as well :slight_smile: !

Tried all of that, to no avail sadly.

Again, didn’t work unfortunately.
Do you have any idea how to get the actual OnscreenText or TextNode used by the DirectGui widget?

That might be helpful, so I certainly wouldn’t mind checking it out, thanks :slight_smile: !

Yes, they definitely seem reasonable enough, until I realized that these are actually the bounds of the text only! (At least, that’s what it seems like.)
There is however another method, widget.guiItem.getFrame, which returns the size of the frame only, so I could just combine these results to get the complete size.
In the hope of getting help from the Panda developers, I reported this issue in this separate topic which includes a code sample; if you run it, you should be able to see what I mean.

Fair enough; thank you for the explanation :slight_smile: !

This seems to be yet another issue that only the Panda devs will be able to straighten out, so I’ve reported it in this new topic also.

1 Like

Most important would be to make the properties available like those of the DirectGui widgets like element[“propertyName”]. Other than that some base functions that come with the DirectGuiBase class will probably be necessary too. I’m currently in the process of extending the editor to support custom widgets, so I’m not yet able to give you all the information as I don’t know it myself yet. But in general it’d probably be good if widgets are derived from DirectGuiBase or any of the DirectGui widgets.

To access it and all other elements created in a widget, you can use the component(componentName) function. Here’s a quick example of how to get the OnscreenText of a Frame.

from direct.gui.DirectFrame import DirectFrame
frame = DirectFrame(text="Test")
frame.component("text0")

Note though that different widgets may have different names for their components. Also keep in mind that the name will have the index of the state appended to it, so if you want the text, the component name usually won’t be “text” but “text0” for the text node of the first state. So for a Button for example you get four states, so there should also be four text components.

1 Like

Indeed, looking into the earlier version of my code it was necessary for onScreenText() and getTightBounds(), now with DirectLabel(), getBounds() returns the bounds even before reparenting…

Well, I’m not actually implementing any new widget types, there’s just a Widget class that is meant to be used as a wrapper around an instance of any existing DirectGuiWidget class. The user only needs to call its constructor once and that’s basically the only time the user will need to interact with it. There are no properties that need to be set on it; it is primarily meant to interact with the second class I’ve implemented for this project: the Sizer class. And this one also has no real user-adjustable properties to speak of, except perhaps a “default size” (a minimal size, even without any content). Modelling that class after a dict-like object just for that seems a bit silly to me, not to mention that I’m already not a fan of how DirectGui widget properties are accessed using subscription; I would have much preferred Python properties, public variables or even getters and setters, but yeah, we’re stuck with this interface now.

That’s great to know, thank you very much for that :slight_smile: !

For now, I’m going to ignore additional states and just assume that the text is the same for all states; in fact, I haven’t checked yet but perhaps you can tell me if the size of a widget is determined by the largest text associated with any of its states, or does it change size depending on the size of the text associated with the state that is made active?

Just took another look at your sample code and yes, there probably aren’t that many properties which should be able to be user adjustable. There are the direction (horizontal/vertical and maybe some more), border size and expand value, which at least in the editor should be changeable at all times and so far the editor is designed to work with directGui like property handling. Aside of that the designer uses other parts too like the guiId to find/select and work with the GUI elements.

The widgets size is initially calculated by the first (0) state. So, if your text is small in that state and you have a longer one in the other states, the text will probably go out of bounds of the actual frame. It won’t be cut off though. Just run the following to see an example of how it will look:

from direct.gui.DirectButton import DirectButton
from direct.showbase.ShowBase import ShowBase
DirectButton(scale=0.1, text=('button','buttonABC','buttonDEF','buttonGHI'))
ShowBase().run()

Fair enough, in the context of an editor that does make sense. I’ll see what I can do.

Ah, I see. That’s one thing less to worry about, then :smiley: .

UPDATE:

Support has been added for DirectScrolledList and DirectScrolledFrame, which can be wrapped in dedicated Widget subclasses to make them work with the layout system. Try out scrolled_list.py and scrolled_frame.py to see them in action.

The GridSizer class can now also be used to arrange your widgets in even more sophisticated ways; see gridsizer.py for a very simple example.

If you need more help than the samples provide, or you just want a more basic and detailed explanation of how the layout system works, be sure to visit the newly added Wiki.

Although everything seems to work quite well at this point, I have to admit that I don’t really know what to do with customized DirectGui widgets that use a geom and/or image instead of their built-in frame. My Widget class resizes objects by adjusting their frameSize, but this has no effect on geoms or images (someone correct me if I’m wrong). In most cases these custom geoms or images probably don’t need to be resized, but if you really do need this, I see only one solution and that is to subclass the Widget class and override its set_size (and possibly set_pos) methods to update their size and position as needed.

@wolf:
The Widget and Sizer classes have been given a dict-like interface to access their unique guiId, while the properties of a sizer (grow_dir and default_size) can also be managed this way.
And while I prefer to promote snake_case for method names, I have nevertheless provided camelCase aliases for those that might be useful for integration into your editor (i.e. Widget.resetFrameSize, ScrolledListWidget.addItem and ScrolledListWidget.removeItem).

2 Likes