Skip to content

Instantly share code, notes, and snippets.

@hynekcer
Created January 14, 2013 08:47
Show Gist options
  • Save hynekcer/4528671 to your computer and use it in GitHub Desktop.
Save hynekcer/4528671 to your computer and use it in GitHub Desktop.
HTML like layouting (StackOverflow question 14264427) http://stackoverflow.com/questions/14264427/html-like-layouting The original source by Niklas R from the question
# coding: utf-8
import copy
import pygame
import argparse
LEFT = 1
def rint(x):
if x % 1 >= 0.5:
x += 1
return int(x)
class NodeToLarge(Exception):
def __init__(self, message, row, node):
super(NodeToLarge, self).__init__(message)
self.row = row
self.node = node
text = '''This is some example text for demonstration purpose.
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod
tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At
vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren,
no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit
amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut
labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam
et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata
sanctus est Lorem ipsum dolor sit amet.'''
class Node(object):
width = 0
height = 0
margin_x = 0
margin_y = 0
x = 0
y = 0
background = None
def __init__(self, options):
super(Node, self).__init__()
self.options = options
def get_real_size(self):
return self.width + self.margin_x * 2, self.height + self.margin_y * 2
def add(self, node):
return False
def layout(self):
pass
def paint(self, rel_x, rel_y, screen):
x, y = self.x + self.margin_x + rel_x, self.y + self.margin_y + rel_y
w, h = self.width, self.height
if self.background and self.options.colorize:
screen.fill(self.background, pygame.Rect(x, y, w, h))
class ContainerNode(Node):
def __init__(self, options):
super(ContainerNode, self).__init__(options)
self.nodes = []
def layout(self):
for node in self.nodes:
node.layout()
def paint(self, rel_x, rel_y, screen):
super(ContainerNode, self).paint(rel_x, rel_y, screen)
for node in self.nodes:
node.paint(rel_x + self.x + self.margin_x, rel_y + self.y + self.margin_y, screen)
class TextNode(Node):
background = (240, 190, 20)
def __init__(self, word, options):
super(TextNode, self).__init__(options)
self.word = word
self.width, self.height = options.font.size(word)
self.height = options.line_height or self.height
self.margin_x = options.font.size(' ')[0] / 2.0
self.margin_y = 0
def add(self, node):
raise NotImplementedError
def paint(self, rel_x, rel_y, screen):
super(TextNode, self).paint(rel_x, rel_y, screen)
options = self.options
rel_x += self.x + self.margin_x
rel_y += self.y + self.margin_y
label = options.font.render(self.word, options.antialias, options.color)
screen.blit(label, (rint(rel_x), rint(rel_y)))
class InlineNodeRow(ContainerNode):
background = (255, 80, 50)
def __init__(self, options):
super(InlineNodeRow, self).__init__(options)
def add(self, node, force=False):
w, h = node.get_real_size()
if (self.options.maxwidth and self.width + w > self.options.maxwidth) and not force:
return False
self.width += w
if h > self.height:
self.height = h
self.nodes.append(node)
return True
def layout(self):
if not self.nodes:
return
options = self.options
justify = self.options.justify
maxwidth = self.options.maxwidth
if justify not in ['left', 'right', 'block', 'center']:
raise ValueError('invalid justify value %s' % justify)
if justify in ['right', 'center'] and not maxwidth:
raise RuntimeError('for %s justification, a maxwidth must have been set.' % justify)
if justify == 'block' and not maxwidth:
justify = 'left'
if justify == 'block':
# Is that really okay?
free_space = maxwidth - self.width
self.width = maxwidth
else:
free_space = 0
if len(self.nodes) > 1:
free_space_partial = float(free_space) / (len(self.nodes) - 1)
else:
free_space_partial = 0
last_x = 0
for node in self.nodes:
w, h = node.get_real_size()
node.x = last_x
last_x = last_x + w + free_space_partial
free_space_h = self.height - h
if options.vertical_align == 'headline':
node.y = 0
elif options.vertical_align == 'middle':
node.y = free_space_h * 0.5
elif options.vertical_align == 'baseline':
node.y = free_space_h
super(InlineNodeRow, self).layout()
class NodeBox(ContainerNode):
background = (30, 100, 255)
def __init__(self, options):
super(NodeBox, self).__init__(options)
self.width = options.maxwidth - self.margin_x * 2
self.sub_options = copy.copy(options)
self.sub_options.maxwidth = self.width
self.c_row = InlineNodeRow(self.sub_options)
def end_line(self):
options = self.c_row.options
if self.c_row.options.justify == 'block':
options = copy.copy(options)
options.justify = 'left'
self.c_row.options = options
self.nodes.append(self.c_row)
self.height += self.c_row.height
self.c_row = InlineNodeRow(self.sub_options)
def add(self, node):
if not self.c_row.add(node):
w, h = self.c_row.get_real_size()
self.height += h
self.nodes.append(self.c_row)
self.c_row = InlineNodeRow(self.sub_options)
if not self.c_row.add(node, True):
raise NodeToLarge('Node is too large for this NodeBox.', self.c_row, node)
return True
def layout(self):
options = self.options
last_y = 0
for node in self.nodes:
w, h = node.get_real_size()
if options.justify in ['left', 'block']:
node.x = 0
elif options.justify == 'right':
node.x = self.options.maxwidth - w - self.margin_x * 2
elif options.justify == 'center':
node.x = (self.options.maxwidth - w) * 0.5 - self.margin_x
else:
raise ValueError('self.options.maxwidth')
node.y = last_y
last_y += h
super(NodeBox, self).layout()
class Options(object):
def __init__(self, color, font, maxwidth, justify='left',
antialias=True, vertical_align='headline', line_height=None,
colorize=False):
super(Options, self).__init__()
self.color = color
self.font = font
self.justify = justify
self.maxwidth = maxwidth
self.antialias = antialias
self.vertical_align = vertical_align
self.line_height = line_height
self.colorize = colorize
def convert_text(text, options):
lines = text.split('\n')
box = NodeBox(options)
for line in lines:
words = line.split()
for word in words:
node = TextNode(word, options)
box.add(node)
box.end_line()
box.layout()
return box
def main():
parser = argparse.ArgumentParser()
parser.add_argument('justify', choices=['left', 'right', 'center', 'block'])
parser.add_argument('-c', '--colorize', action='store_true')
parser.add_argument('-f', '--font', default='Times New Roman')
parser.add_argument('-s', '--font-size', default=14, type=int)
parser.add_argument('-v', '--valign', choices=['baseline', 'middle', 'headline'])
args = parser.parse_args()
pygame.init()
pygame.font.init()
screen = pygame.display.set_mode((600, 600))
pygame.display.set_caption('Font Drawing Test')
options = Options(
maxwidth=600,
color=(0, 0, 0),
font=pygame.font.SysFont(args.font, args.font_size),
justify=args.justify,
vertical_align=args.valign,
colorize=args.colorize,
)
position = [0, 0]
def make_box():
options.maxwidth = 600 - position[0]
return convert_text(text, options)
box = make_box()
mouse_down = False
clock = pygame.time.Clock()
running = True
while running:
clock.tick(30)
screen.fill((255, 255, 255))
box = make_box()
box.paint(position[0], position[1], screen)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == LEFT:
mouse_down = True
elif event.type == pygame.MOUSEBUTTONUP and event.button == LEFT:
mouse_down = False
elif event.type == pygame.MOUSEMOTION:
if mouse_down:
position = event.pos
pygame.display.flip()
if __name__ == '__main__':
main()
@hynekcer
Copy link
Author

The exception NodeToLarge is never raised because force is True in c_row.add(...).

@hynekcer
Copy link
Author

Attributes width, height, margin_x, margin_y, white_space, background should be attributes of Node instance not of Node class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment