mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
optimized layout_resolve added tests
This commit is contained in:
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from typing import cast, List, Optional, Sequence
|
from typing import cast, Sequence
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
@@ -13,12 +13,12 @@ else:
|
|||||||
class Edge(Protocol):
|
class Edge(Protocol):
|
||||||
"""Any object that defines an edge (such as Layout)."""
|
"""Any object that defines an edge (such as Layout)."""
|
||||||
|
|
||||||
size: Optional[int] = None
|
size: int | None
|
||||||
fraction: int = 1
|
fraction: int = 1
|
||||||
min_size: int = 1
|
min_size: int = 1
|
||||||
|
|
||||||
|
|
||||||
def layout_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
|
def layout_resolve(total: int, edges: Sequence[Edge]) -> list[int]:
|
||||||
"""Divide total space to satisfy size, fraction, and min_size, constraints.
|
"""Divide total space to satisfy size, fraction, and min_size, constraints.
|
||||||
|
|
||||||
The returned list of integers should add up to total in most cases, unless it is
|
The returned list of integers should add up to total in most cases, unless it is
|
||||||
@@ -37,33 +37,37 @@ def layout_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
|
|||||||
# Size of edge or None for yet to be determined
|
# Size of edge or None for yet to be determined
|
||||||
sizes = [(edge.size or None) for edge in edges]
|
sizes = [(edge.size or None) for edge in edges]
|
||||||
|
|
||||||
_Fraction = Fraction
|
if None not in sizes:
|
||||||
|
return cast(list[int], sizes)
|
||||||
|
|
||||||
# While any edges haven't been calculated
|
# Get flexible edges and index to map these back on to sizes list
|
||||||
while None in sizes:
|
flexible_edges = [
|
||||||
# Get flexible edges and index to map these back on to sizes list
|
(index, edge)
|
||||||
flexible_edges = [
|
for index, (size, edge) in enumerate(zip(sizes, edges))
|
||||||
(index, edge)
|
if size is None
|
||||||
for index, (size, edge) in enumerate(zip(sizes, edges))
|
]
|
||||||
if size is None
|
# Remaining space in total
|
||||||
|
remaining = total - sum(size or 0 for size in sizes)
|
||||||
|
if remaining <= 0:
|
||||||
|
# No room for flexible edges
|
||||||
|
return [
|
||||||
|
((edge.min_size or 1) if size is None else size)
|
||||||
|
for size, edge in zip(sizes, edges)
|
||||||
]
|
]
|
||||||
# Remaining space in total
|
|
||||||
remaining = total - sum(size or 0 for size in sizes)
|
_Fraction = Fraction
|
||||||
if remaining <= 0:
|
while None in sizes:
|
||||||
# No room for flexible edges
|
|
||||||
return [
|
|
||||||
((edge.min_size or 1) if size is None else size)
|
|
||||||
for size, edge in zip(sizes, edges)
|
|
||||||
]
|
|
||||||
# Calculate number of characters in a ratio portion
|
# Calculate number of characters in a ratio portion
|
||||||
portion = _Fraction(
|
portion = _Fraction(
|
||||||
remaining, sum((edge.fraction or 1) for _, edge in flexible_edges)
|
remaining, sum((edge.fraction or 1) for _, edge in flexible_edges)
|
||||||
)
|
)
|
||||||
|
|
||||||
# If any edges will be less than their minimum, replace size with the minimum
|
# If any edges will be less than their minimum, replace size with the minimum
|
||||||
for index, edge in flexible_edges:
|
for flexible_index, (index, edge) in enumerate(flexible_edges):
|
||||||
if portion * edge.fraction <= edge.min_size:
|
if portion * edge.fraction <= edge.min_size:
|
||||||
sizes[index] = edge.min_size
|
sizes[index] = edge.min_size
|
||||||
|
remaining -= edge.min_size
|
||||||
|
del flexible_edges[flexible_index]
|
||||||
# New fixed size will invalidate calculations, so we need to repeat the process
|
# New fixed size will invalidate calculations, so we need to repeat the process
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -72,9 +76,8 @@ def layout_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
|
|||||||
# to the following line
|
# to the following line
|
||||||
remainder = _Fraction(0)
|
remainder = _Fraction(0)
|
||||||
for index, edge in flexible_edges:
|
for index, edge in flexible_edges:
|
||||||
size, remainder = divmod(portion * edge.fraction + remainder, 1)
|
sizes[index], remainder = divmod(portion * edge.fraction + remainder, 1)
|
||||||
sizes[index] = size
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# Sizes now contains integers only
|
# Sizes now contains integers only
|
||||||
return cast(List[int], sizes)
|
return cast(list[int], sizes)
|
||||||
|
|||||||
64
tests/test_layout_resolve.py
Normal file
64
tests/test_layout_resolve.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from textual._layout_resolve import layout_resolve
|
||||||
|
|
||||||
|
|
||||||
|
class Edge(NamedTuple):
|
||||||
|
size: int | None = None
|
||||||
|
fraction: int = 1
|
||||||
|
min_size: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_single():
|
||||||
|
# One edge fixed size
|
||||||
|
assert layout_resolve(100, [Edge(10)]) == [10]
|
||||||
|
# One edge fraction of 1
|
||||||
|
assert layout_resolve(100, [Edge(None, 1)]) == [100]
|
||||||
|
# One edge fraction 3
|
||||||
|
assert layout_resolve(100, [Edge(None, 2)]) == [100]
|
||||||
|
# One edge, fraction1, min size 20
|
||||||
|
assert layout_resolve(100, [Edge(None, 1, 20)]) == [100]
|
||||||
|
# One edge fraction 1, min size 120
|
||||||
|
assert layout_resolve(100, [Edge(None, 1, 120)]) == [120]
|
||||||
|
|
||||||
|
|
||||||
|
def test_two():
|
||||||
|
# Two edges fixed size
|
||||||
|
assert layout_resolve(100, [Edge(10), Edge(20)]) == [10, 20]
|
||||||
|
# Two edges, fraction 1 each
|
||||||
|
assert layout_resolve(100, [Edge(None, 1), Edge(None, 1)]) == [50, 50]
|
||||||
|
# Two edges, one with fraction 2, one with fraction 1
|
||||||
|
# Note first value is rounded down, second is rounded up
|
||||||
|
assert layout_resolve(100, [Edge(None, 2), Edge(None, 1)]) == [66, 34]
|
||||||
|
# Two edges, both with fraction 2
|
||||||
|
assert layout_resolve(100, [Edge(None, 2), Edge(None, 2)]) == [50, 50]
|
||||||
|
# Two edges, one with fraction 3, one with fraction 1
|
||||||
|
assert layout_resolve(100, [Edge(None, 3), Edge(None, 1)]) == [75, 25]
|
||||||
|
# Two edges, one with fraction 3, one with fraction 1, second with min size of 30
|
||||||
|
assert layout_resolve(100, [Edge(None, 3), Edge(None, 1, 30)]) == [70, 30]
|
||||||
|
# Two edges, one with fraction 1 and min size 30, one with fraction 3
|
||||||
|
assert layout_resolve(100, [Edge(None, 1, 30), Edge(None, 3)]) == [30, 70]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"size, edges, result",
|
||||||
|
[
|
||||||
|
(10, [Edge(None, 1), Edge(None, 1), Edge(None, 1)], [3, 3, 4]),
|
||||||
|
(10, [Edge(5), Edge(None, 1), Edge(None, 1)], [5, 2, 3]),
|
||||||
|
(10, [Edge(None, 2), Edge(None, 1), Edge(None, 1)], [5, 2, 3]),
|
||||||
|
(10, [Edge(None, 2), Edge(3), Edge(None, 1)], [4, 3, 3]),
|
||||||
|
(
|
||||||
|
10,
|
||||||
|
[Edge(None, 2), Edge(None, 1), Edge(None, 1), Edge(None, 1)],
|
||||||
|
[4, 2, 2, 2],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
10,
|
||||||
|
[Edge(None, 4), Edge(None, 1), Edge(None, 1), Edge(None, 1)],
|
||||||
|
[5, 2, 1, 2],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_multiple(size, edges, result):
|
||||||
|
assert layout_resolve(size, edges) == result
|
||||||
Reference in New Issue
Block a user