So I wanted to write some simple layout handler classes for the 2D scene graph. For example, a node that automatically arranges all its child nodes in a line, or a grid, etc. It’s not difficult, but it doesn’t seem to be well documented anywhere how to do this with examples. So people might find this useful, and I’m also sort of hoping people will look at it and tell me what I did wrong or could do better.
First, some things I discovered:
OnscreenImage objects can be aligned using the getPos(), setPos() and getTightBounds() methods. (btw, getTightBounds does not appear in the API.)
OnscreenText objects are less amenable, because the getPos() method is annoying. Rather than a Point3 like OnscreenImage, OnscreenText.getPos() returns either a 2-tuple or a 3-tuple of floats depending on whether you specified a pos when initialising the object. No matter, there is DirectLabel and TextNode to use instead.
For DirectGUI objects the getTightBounds() method does not work, it always returns a bunch of zeros. Fortunately the getBounds() method does work. I tested DirectLabel, DirectButton and DirectEntry, and they all seem to work.
So the code I’ve written is for laying out DirectGUI objects. It’s a partial but working and extendable implementation of the theory of packing boxes layout mechanism from GTK+ (which I only glanced at very briefly, but it looked like a nice idea).
Here’s the code:
"""
layouts.py
A partial implementation of the theory of packing boxes (for laying out
objects in two dimensions) from GTK+. This is meant for laying out
DirectGUI objects, but you can pack any NodePath that implements
getBounds() sensibly.
Theory Of Packing Boxes
-----------------------
From GTK+. Boxes are invisible GUI widgets into which you pack other
widgets to achieve a desired layout. In a horizontal box (hbox) objects
are packed from left to right or right to left. In a vertical box (vbox)
objects are packed from top to bottom or bottom to top. You can pack
objects in both directions in the same box at the same time by using the
two different pack methods. Options control whether objects are packed
as tightly possible, or whether the objects are spread out to fill the
amount of space alloted to the box, etc.
Finally, a table is a type of box that manages a number of cells in rows
and columns. Objects are packed into the cells. A single object can
occupy multiple cells. When packing an object, you specify the range of
cells it will occupy by specifying left, right, bottom, and top cells.
What's implemented so far:
* hboxes with packing from left to right.
* vboxes with packing from top to bottom.
* You can pack boxes into boxes in any combination. Except that you
cannot pack a box into itself.
Todo:
* Support packing in both directions.
* Option to pack objects tightly or space them out to fill a given
width.
* Option to insert fixed-size margins between objects.
* Vertical alignment option for hboxes and horizontal alignment option
for vboxes.
* Table box class.
"""
from pandac.PandaModules import *
from direct.gui.DirectGui import *
LEFT, RIGHT, BOTTOM, TOP = 0, 1, 2, 3
class Box(DirectFrame):
"""
Base class for HBox and VBox. Not meant to be instantiated itself.
"""
def __init__(self):
DirectFrame.__init__(self)
# We maintain our own list of objects packed into this box,
# rather than reusing the scene graph. This means we don't have
# to capture new children that are added to this node in the
# scene graph using (for example) reparentTo, and it means that
# users can add children to this node without them being packed
# in the box.
self.objects = []
# The getBounds() method of DirectFrame does not return anything
# useful so we maintain our own bounds.
self.bounds = [0.0,0.0,0.0,0.0] # left,right,bottom,top
def pack(self,obj):
"""
Pack a new object into this box.
Box.pack handles updating the bounds of the box, appending the
new object to the list of packed objects, and updating the scene
graph. It calls layout() to allow the new object to be
positioned, subclasses should override layout to implement
different layouts.
"""
self.layout(obj)
# Get the bounds of the new object.
left,right,bottom,top = obj.getBounds()
# Translate them into the coordinate space of this box.
top_right = Point3(right,0,top)
bottom_left = Point3(left,0,bottom)
left = self.getRelativePoint(obj,bottom_left).getX()
bottom = self.getRelativePoint(obj,bottom_left).getZ()
right = self.getRelativePoint(obj,top_right).getX()
top = self.getRelativePoint(obj,top_right).getZ()
# Update the bounds of this box to encompass the new object.
if left < self.bounds[LEFT]: self.bounds[LEFT] = left
if right > self.bounds[RIGHT]: self.bounds[RIGHT] = right
if bottom < self.bounds[BOTTOM]: self.bounds[BOTTOM] = bottom
if top > self.bounds[TOP]: self.bounds[TOP] = top
# Add the object to the list of packed objects.
self.objects.append(obj)
# Update the scene graph.
obj.reparentTo(self)
def layout(self,obj):
"""
Position a new object that is being packed into this box.
Subclasses should override this method to implement their
own layouts.
"""
pass
def getBounds(self):
return self.bounds
class HBox(Box):
"""
A horizontal container. Objects that are packed into this box will
be layed out along a horizontal line.
"""
def layout(self,obj):
"""
Align the left side of the new object with the right side of
the last packed object, and align the top of the new object
with the top of the last packed object.
"""
# FIXME: Use Panda's getRelativePoint() to translate between
# coordinate spaces instead of doing it manually everywhere.
if self.objects == []:
# This is the first object to be packed. Align it with this
# empty box.
right = self.getBounds()[RIGHT]
top = self.getBounds()[TOP]
else:
# Align the new object with the last object that was packed.
last = self.objects[-1]
right = last.getBounds()[RIGHT]
right *= last.getScale().getX()
right += last.getPos().getX()
top = last.getBounds()[TOP]
top *= last.getScale().getZ()
top += last.getPos().getZ()
# Align the left of the new object with `right`.
left = obj.getBounds()[LEFT]
left *= obj.getScale().getX()
left += obj.getPos().getX()
distance = right - left
obj.setPos(obj.getPos() + Point3(distance,0,0))
# Align the top of the new object with `top`.
t = obj.getBounds()[TOP]
t *= obj.getScale().getZ()
t += obj.getPos().getZ()
distance = top - t
obj.setPos(obj.getPos() + Point3(0,0,distance))
class VBox(Box):
"""
A vertical container. Objects that are packed into this box will
be layed out along a vertical line.
"""
def layout(self,obj):
"""
Align the top side of the new object with the bottom side of
the last packed object, and align the left of the new object
with the left of the last packed object.
"""
# FIXME: Use Panda's getRelativePoint() to translate between
# coordinate spaces instead of doing it manually everywhere.
if self.objects == []:
# This is the first object to be packed. Align it with this
# empty box.
bottom = self.getBounds()[BOTTOM]
left = self.getBounds()[LEFT]
else:
# Align the new object with the last object that was packed.
last = self.objects[-1]
bottom = last.getBounds()[BOTTOM]
bottom *= last.getScale().getZ()
bottom += last.getPos().getZ()
left = last.getBounds()[LEFT]
left *= last.getScale().getX()
left += last.getPos().getX()
# Align the top of the new object with `bottom`.
top = obj.getBounds()[TOP]
top *= obj.getScale().getZ()
top += obj.getPos().getZ()
distance = bottom - top
obj.setPos(obj.getPos() + Point3(0,0,distance))
# Align the left of the new object with `left`.
l = obj.getBounds()[LEFT]
l *= obj.getScale().getX()
l += obj.getPos().getX()
distance = left - l
obj.setPos(obj.getPos() + Point3(distance,0,0))
if __name__== '__main__' :
"""
Run a demo showing some DirectGUI objects packed into vertical and
horizontal boxes, and boxes packed into boxes.
"""
import direct.directbase.DirectStart
# Make 3 vboxes, each containing 5 DirectGUI objects, and pack them
# all into an hbox.
hbox = HBox()
hbox.setPos(-1.3,0,0.9)
for x in range(3):
vbox = VBox()
dl = DirectLabel(text='Hellooooo!',scale=.05)
vbox.pack(dl)
de = DirectEntry(initialText='Hello Text!',scale=.07,pos=(-1,0,-.6))
vbox.pack(de)
db = DirectButton(text="I'm a button",scale=.07,pos=(.8,0,0))
vbox.pack(db)
another_dl = DirectLabel(text='Hello again',scale=.05)
vbox.pack(another_dl)
another_de = DirectEntry(initialText='Blorg',scale=.07,pos=(-1,0,-.6))
vbox.pack(another_de)
hbox.pack(vbox)
# Make 3 hboxes, each containing 5 DirectGUI objects, and pack them
# all into aa vbox.
vbox = VBox()
vbox.setPos(-1.3,0,0)
for x in range(3):
hbox = HBox()
dl = DirectLabel(text='Hellooooo!',scale=.05)
hbox.pack(dl)
de = DirectEntry(initialText='Hello Text!',scale=.07,pos=(-1,0,-.6))
hbox.pack(de)
db = DirectButton(text="I'm a button",scale=.07,pos=(.8,0,0))
hbox.pack(db)
another_dl = DirectLabel(text='Hello again',scale=.05)
hbox.pack(another_dl)
another_de = DirectEntry(initialText='Blorg',scale=.07,pos=(-1,0,-.6))
hbox.pack(another_de)
vbox.pack(hbox)
run()