1. 4FIPS
  2. PHOTOS
  3. VIDEOS
  4. APPS
  5. CODE
  6. FORUMS
  7. ABOUT
#!/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