#!/usr/bin/env python
#[DECLARATIVE UI LAYOUT ENGINE, URL: http://forums.4fips.com/viewtopic.php?f=3&t=6896]
#[CODE BY FIPS @ 4FIPS.COM, (c) 2016 FILIP STOKLAS, MIT-LICENSED]
from collections import namedtuple
Vec2 = namedtuple("Vec2", "x y")
Rect = namedtuple("Rect", "x y w h")
Margin = namedtuple("Margin", "l t r b")
class Layout(object):
def __init__(self, children, min_size=Vec2(0, 0), stretch=Vec2(1, 1)):
self.children = children
self.min_size = min_size
self.stretch = stretch
self.exp_size = None # product of expand()
self.rect = None # product of parent's layout()
def reshape(self, rect):
self.expand()
self.layout(rect)
def expand(self):
for ch in self.children:
ch.expand()
self.expand_children()
def layout(self, rect):
self.layout_children(rect)
for ch in self.children:
ch.layout(ch.rect)
@staticmethod
def spread(target_size, size_stretch_pairs):
sizes, stretches = zip(*size_stretch_pairs)
fixed_size = sum(sz for sz in sizes)
flexy_size = max(0, target_size - fixed_size)
num_flexy_parts = sum(st for st in stretches)
flexy_add = flexy_size // num_flexy_parts if num_flexy_parts else 0
flexy_adds = [st * flexy_add for st in stretches]
flexy_err = flexy_size - sum(flexy_adds)
if flexy_err: # distribute the rounding error introduced by fixed-point division
flexy_adds[[idx for idx, st in enumerate(stretches) if st][-1]] += flexy_err
return [sum(sz_fa) for sz_fa in zip(sizes, flexy_adds)]
class Elem(Layout):
def __init__(self, widget, min_size=Vec2(0, 0), stretch=Vec2(1, 1)):
super(Elem, self).__init__([], min_size, stretch)
widget.meta_elem = self
self.widget = widget
def expand_children(self):
self.exp_size = self.min_size
def layout_children(self, rect):
pass
class Border(Layout):
def __init__(self, children, min_size=Vec2(0, 0), stretch=Vec2(1, 1), margin=Margin(0, 0, 0, 0)):
super(Border, self).__init__(children, min_size, stretch)
self.margin = margin
def expand_children(self):
w, h = self.margin.l + self.margin.r, self.margin.t + self.margin.b
for ch in self.children:
w = max(w, ch.exp_size.x)
h = max(h, ch.exp_size.y)
self.exp_size = Vec2(w, h)
def layout_children(self, rect):
l, t, r, b, = self.margin
for ch in self.children:
ch.rect = Rect(rect.x + l, rect.y + t, rect.w - l - r, rect.h - t - b)
class VStack(Layout):
def __init__(self, children, min_size=Vec2(0, 0), stretch=Vec2(1, 1)):
super(VStack, self).__init__(children, min_size, stretch)
def expand_children(self):
w, h = 0, 0
for ch in self.children:
w = max(w, ch.exp_size.x)
h += ch.exp_size.y
self.exp_size = Vec2(w, h)
def layout_children(self, rect):
x, y = rect.x, rect.y
ch_heights = Layout.spread(rect.h, [(ch.exp_size.y, ch.stretch.y) for ch in self.children])
for ch, h in zip(self.children, ch_heights):
w = rect.w if ch.stretch.x else ch.exp_size.x
ch.rect = Rect(x, y, w, h)
y += h
class HStack(Layout):
def __init__(self, children, min_size=Vec2(0, 0), stretch=Vec2(1, 1)):
super(HStack, self).__init__(children, min_size, stretch)
def expand_children(self):
w, h = 0, 0
for ch in self.children:
w += ch.exp_size.x
h = max(h, ch.exp_size.y)
self.exp_size = Vec2(w, h)
def layout_children(self, rect):
x, y = rect.x, rect.y
ch_widths = Layout.spread(rect.w, [(ch.exp_size.x, ch.stretch.x) for ch in self.children])
for ch, w in zip(self.children, ch_widths):
h = rect.h if ch.stretch.y else ch.exp_size.y
ch.rect = Rect(x, y, w, h)
x += w