from dataclasses import dataclass
from functools import lru_cache, partial
from typing import Callable, Iterable
from .classes import *
__all__ = [
"flex",
"flex_custom",
"vbox_flex",
"hbox_flex",
"hbox_flex_wrap"
]
@dataclass(frozen=True, eq=True)
class Flex:
"""Extra data to mark layouts as flexible for flex containers
Note:
You are highly unlikely to use this class directly. Consider using :obj:`flex` or :obj:`flex_custom`.
"""
node: Layout
grow: int
shrink: bool
basis: bool
[docs]
def flex_custom(grow=1, shrink=True, basis=False) -> Callable[[Layout], Flex]:
"""Wrap child layout in a :obj:`Flex` class to mark it as flexible and adjust attributes.
Note:
This node is only useful with flex containers (:obj:`vbox_flex` and :obj:`hbox_flex`).
Args:
grow:
If there is any leftover space, this value is used to determine how
much of this space does this child get relative to other children.
shrink:
Should this child shrink if container does not fit into the
available space? This property has an effect only when ``basis`` is
set to ``True``.
basis:
If true, container will take this children's minimum size into account.
If false, container will assume minimum size of 0.
"""
def out(node: Layout):
return Flex(node, grow, shrink, basis)
return out
[docs]
def flex(node: Layout) -> Flex:
"""Wrap layout in a :obj:`Flex` class to mark it as flexible.
Equivalent to :obj:`flex_custom` ``(grow=1 shrink=True, basis=False)``.
Note:
This node is only useful with flex containers (:obj:`vbox_flex` and :obj:`hbox_flex`).
"""
return flex_custom(1, True, False)(node)
def _calculate_flex_children_sizes(
available_space: int,
children: Iterable[Flex],
child_basis: Iterable[int],
) -> Iterable[int]:
leftover_space = available_space - sum(child_basis)
child_data = list(zip(child_basis, children))
if leftover_space < 0: # shrink
# missing_space = -1 * leftover_space
fixed_space = sum(basis for (basis, child) in child_data if child.shrink == 0)
flexible_space = available_space - fixed_space
children_that_will_shrink = [i for i in child_data if i[1].shrink]
# shrink_factor equation explanation
# a -> flexible_space
# k -> shrink factor
# bₙ -> basis size of shrinkable child n
# after elements have ben shrunk by factor k,
# the below eqation should be true.
# a = b₁k + ... + bₙk
# <=> a = k(b₁ + ... + bₙ)
# <=> k = a/(b₁ + ... + bₙ)
basis_sum = sum(
basis for (basis, child) in children_that_will_shrink
)
if basis_sum == 0:
return child_basis
shrink_factor = flexible_space / basis_sum
shrunk_children = []
calculated_flexible_space = 0
for (basis, child) in children_that_will_shrink:
child_size = round(basis * shrink_factor)
calculated_flexible_space += child_size
shrunk_children.append(child_size)
# because child sizes are actually floats that have been rounded to ints
# there may be a rounding error that needs to be handled
rounding_error = flexible_space - calculated_flexible_space
if rounding_error:
rounding_rations = even_divide(rounding_error, len(shrunk_children))
shrunk_children = [a + b for (a, b) in zip(shrunk_children, rounding_rations)]
shrunk_children = list(reversed(shrunk_children)) # so that pop is in order
out = []
for basis, child in child_data:
if child.shrink == 0:
out.append(basis)
else:
out.append(shrunk_children.pop())
return out
# grow
total_grow = sum(i.grow for i in children)
space_rations = even_divide(leftover_space, total_grow)
out = []
for (basis, child) in child_data:
out.append(basis + sum(space_rations.pop() for _ in range(child.grow)))
return out
[docs]
def vbox_flex(children: Iterable[Flex | Layout]) -> Layout:
"""A container node that allows children to expand on the y axis.
Modeled of the CSS flexbox layout model. By default acts as a regular
:obj:`~functui.common.vbox`. Wrap children in :func:`flex` or
:func:`flex_custom` wrapper nodes to allow them to use leftover space if
there is any.
"""
children = tuple(child if isinstance(child, Flex) else flex_custom(0, False, True)(child) for child in children)
def _min_size(measure_text: MeasureTextFunc, from_size: Rect):
child_basis = [(i.node.min_size(measure_text, from_size).height if
i.basis else 0) for i in children]
child_heights = _calculate_flex_children_sizes(from_size.height, children, child_basis)
child_widths = []
for (height, child) in zip(child_heights, children):
rect = child.node.min_size(
measure_text,
Rect(from_size.width, height)
)
child_widths.append(rect.width)
return Rect(max(child_widths), sum(child_heights))
return Layout(
func=vbox_flex,
min_size=_min_size,
render=partial(_vbox_flex_render, children)
)
@lru_cache(LRU_MAX_SIZE)
def _vbox_flex_render(children: tuple[Flex, ...], frame: Frame, box: Box):
child_basis = [(i.node.min_size(frame.measure_text, box.rect).height if
i.basis else 0) for i in children]
child_heights = _calculate_flex_children_sizes(box.height, children, child_basis)
res = Result()
at_y = 0
for child, child_height in zip(children, child_heights):
child_box = Box(box.width, child_height, box.position + Coordinate(0, at_y))
at_y += child_box.height
res.add_children_after([child.node.render(frame.shrink_to(child_box), child_box)])
return res
[docs]
def hbox_flex(children: Iterable[Flex | Layout], /):
"""A container node that allows children to expand on the x axis.
Modeled of the CSS flexbox layout model. By default acts as a regular
:obj:`~functui.common.hbox`. Wrap children in :func:`flex` or
:func:`flex_custom` wrapper nodes to allow them to use leftover space if
there is any.
Examples:
Usage with flex:
>>> from functui import Rect, layout_to_str
>>> from functui.common import border, text
>>> from functui.flex import flex, hbox_flex, flex_custom
>>> layout = hbox_flex([
... text("Flex.") | border | flex,
... text("No flex.") | border,
... ]) | border
>>> print(layout_to_str(layout, Rect(40, 5)))
┌──────────────────────────────────────┐
│┌──────────────────────────┐┌────────┐│
││Flex. ││No flex.││
│└──────────────────────────┘└────────┘│
└──────────────────────────────────────┘
Usage with flex_custom grow argument:
>>> layout = hbox_flex([
... text("grow 1") | border | flex_custom(grow=1),
... text("grow 2") | border | flex_custom(grow=2),
... text("grow 1") | border | flex, # flex same as flex_custom(1)
... ]) | border
>>> print(layout_to_str(layout, Rect(40, 5)))
┌──────────────────────────────────────┐
│┌───────┐┌─────────────────┐┌────────┐│
││grow 1 ││grow 2 ││grow 1 ││
│└───────┘└─────────────────┘└────────┘│
└──────────────────────────────────────┘
Usage with flex_custom grow and basis arguments:
>>> layout = hbox_flex([
... text("basis and grow") | border | flex_custom(grow=1, basis=True),
... text("grow") | border | flex, # flex is same as flex_custom(grow=1)
... ]) | border
>>> print(layout_to_str(layout, Rect(40, 5)))
┌──────────────────────────────────────┐
│┌─────────────────────────┐┌─────────┐│
││basis and grow ││grow ││
│└─────────────────────────┘└─────────┘│
└──────────────────────────────────────┘
"""
children = tuple(child if isinstance(child, Flex) else flex_custom(0, False, True)(child) for child in children)
def _min_size(measure_text: MeasureTextFunc, from_size: Rect):
child_basis = [(i.node.min_size(measure_text, from_size).width if
i.basis else 0) for i in children]
child_widths = _calculate_flex_children_sizes(from_size.width, children, child_basis)
child_heights = []
for (width, child) in zip(child_widths, children):
rect = child.node.min_size(
measure_text,
Rect(width, from_size.height)
)
child_heights.append(rect.height)
return Rect(sum(child_widths), max(child_heights))
return Layout(
func=hbox_flex,
min_size=_min_size,
render=partial(_hbox_flex_render, children)
)
@lru_cache(LRU_MAX_SIZE)
def _hbox_flex_render(children: Iterable[Flex], frame: Frame, box: Box):
child_basis = [(i.node.min_size(frame.measure_text, box.rect).width if
i.basis else 0) for i in children]
child_widths = _calculate_flex_children_sizes(box.width, children, child_basis)
res = Result()
at_x = 0
for child, child_width in zip(children, child_widths):
child_box = Box(child_width, box.height, box.position + Coordinate(at_x, 0))
at_x += child_box.width
res.add_children_after([child.node.render(frame.shrink_to(child_box), child_box)])
return res
@dataclass
class _FlexData:
bounding_rect: Rect
flex_children: list[Flex]
def _split_flex_by_lines_h(available_space: int, children: Iterable[Flex], measure_text: MeasureTextFunc):
flex_by_lines = [_FlexData(Rect(0, 0), [])]
for flex in children:
current_flex_data = flex_by_lines[-1]
if not flex.basis:
current_flex_data.flex_children.append(flex)
continue
child_min_width, child_min_height = flex.node.min_size(
measure_text, Rect(available_space, 999999))
if current_flex_data.bounding_rect.width + child_min_width > available_space:
flex_by_lines.append(_FlexData(
Rect(child_min_width, child_min_height),
[flex])
)
else:
current_flex_data.bounding_rect = Rect(
current_flex_data.bounding_rect.width + child_min_width,
max(current_flex_data.bounding_rect.height, child_min_height)
)
current_flex_data.flex_children.append(flex)
return flex_by_lines
[docs]
def hbox_flex_wrap(children: Iterable[Flex | Layout]) -> Layout:
"""A container node that allows children to wrap vertically.
Modeled of the CSS flexbox layout model. If all children can't fit into the
available horizontal space, wrap them. Wrap children in :func:`flex` or
:func:`flex_custom` wrapper nodes to allow them to use leftover space if
there is any.
"""
children = tuple(child if isinstance(child, Flex) else flex_custom(0, False, True)(child) for child in children)
def min_size(measure_text: MeasureTextFunc, from_rect: Rect):
lines = _split_flex_by_lines_h(from_rect.width, children, measure_text)
return Rect(
width=max(i.bounding_rect.width for i in lines),
height=sum(i.bounding_rect.height for i in lines),
)
return Layout(
func=hbox_flex_wrap,
min_size=min_size,
render=partial(_hbox_flex_wrap_render, children)
)
@lru_cache(LRU_MAX_SIZE)
def _hbox_flex_wrap_render(children: Iterable[Flex], frame: Frame, box: Box):
#
# split by 'lines'
#
children_by_lines = _split_flex_by_lines_h(box.width, children, frame.measure_text)
at_y = 0
res = Result()
results = []
for data in children_by_lines:
row_rect = Rect(box.width, data.bounding_rect.height)
row_box = Box.from_rect(row_rect, box.position + Coordinate(0, at_y))
results.append(
_hbox_flex_render(
tuple(data.flex_children),
frame.shrink_to(row_box),
row_box
)
)
at_y += data.bounding_rect.height
res.add_children_after(results)
return res