Module textual.layouts.grid

Expand source code
from __future__ import annotations

from fractions import Fraction
from typing import TYPE_CHECKING, Iterable

from .._layout import ArrangeResult, Layout, WidgetPlacement
from .._resolve import resolve
from ..css.scalar import Scalar
from ..geometry import Region, Size, Spacing

if TYPE_CHECKING:
    from ..widget import Widget


class GridLayout(Layout):
    """Used to layout Widgets in to a grid."""

    name = "grid"

    def arrange(
        self, parent: Widget, children: list[Widget], size: Size
    ) -> ArrangeResult:
        styles = parent.styles
        row_scalars = styles.grid_rows or [Scalar.parse("1fr")]
        column_scalars = styles.grid_columns or [Scalar.parse("1fr")]
        gutter_horizontal = styles.grid_gutter_horizontal
        gutter_vertical = styles.grid_gutter_vertical
        table_size_columns = max(1, styles.grid_size_columns)
        table_size_rows = styles.grid_size_rows
        viewport = parent.screen.size

        def cell_coords(column_count: int) -> Iterable[tuple[int, int]]:
            """Iterate over table coordinates ad infinitum.

            Args:
                column_count (int): Number of columns

            """
            row = 0
            while True:
                for column in range(column_count):
                    yield (column, row)
                row += 1

        def widget_coords(
            column_start: int, row_start: int, columns: int, rows: int
        ) -> set[tuple[int, int]]:
            """Get coords occupied by a cell.

            Args:
                column_start (int): Start column.
                row_start (int): Start_row.
                columns (int): Number of columns.
                rows (int): Number of rows.

            Returns:
                set[tuple[int, int]]: Set of coords.
            """
            return {
                (column, row)
                for column in range(column_start, column_start + columns)
                for row in range(row_start, row_start + rows)
            }

        def repeat_scalars(scalars: Iterable[Scalar], count: int) -> list[Scalar]:
            """Repeat an iterable of scalars as many times as required to return
            a list of `count` values.

            Args:
                scalars (Iterable[T]): Iterable of values.
                count (int): Number of values to return.

            Returns:
                list[T]: A list of values.
            """
            limited_values = list(scalars)[:]
            while len(limited_values) < count:
                limited_values.extend(scalars)
            return limited_values[:count]

        cell_map: dict[tuple[int, int], tuple[Widget, bool]] = {}
        cell_size_map: dict[Widget, tuple[int, int, int, int]] = {}

        column_count = table_size_columns
        next_coord = iter(cell_coords(column_count)).__next__
        cell_coord = (0, 0)
        column = row = 0

        for child in children:
            child_styles = child.styles
            column_span = child_styles.column_span or 1
            row_span = child_styles.row_span or 1
            # Find a slot where this cell fits
            # A cell on a previous row may have a row span
            while True:
                column, row = cell_coord
                coords = widget_coords(column, row, column_span, row_span)
                if cell_map.keys().isdisjoint(coords):
                    for coord in coords:
                        cell_map[coord] = (child, coord == cell_coord)
                    cell_size_map[child] = (
                        column,
                        row,
                        column_span - 1,
                        row_span - 1,
                    )
                    break
                else:
                    cell_coord = next_coord()
                    continue
            cell_coord = next_coord()

        # Resolve columns / rows
        columns = resolve(
            repeat_scalars(column_scalars, table_size_columns),
            size.width,
            gutter_vertical,
            size,
            viewport,
        )
        rows = resolve(
            repeat_scalars(
                row_scalars, table_size_rows if table_size_rows else row + 1
            ),
            size.height,
            gutter_horizontal,
            size,
            viewport,
        )

        placements: list[WidgetPlacement] = []
        add_placement = placements.append
        fraction_unit = Fraction(1)
        widgets: list[Widget] = []
        add_widget = widgets.append
        max_column = len(columns) - 1
        max_row = len(rows) - 1
        margin = Spacing()
        for widget, (column, row, column_span, row_span) in cell_size_map.items():
            x = columns[column][0]
            if row > max_row:
                break
            y = rows[row][0]
            x2, cell_width = columns[min(max_column, column + column_span)]
            y2, cell_height = rows[min(max_row, row + row_span)]
            cell_size = Size(cell_width + x2 - x, cell_height + y2 - y)
            width, height, margin = widget._get_box_model(
                cell_size,
                viewport,
                fraction_unit,
            )
            region = (
                Region(x, y, int(width + margin.width), int(height + margin.height))
                .shrink(margin)
                .clip_size(cell_size)
            )
            add_placement(WidgetPlacement(region, margin, widget))
            add_widget(widget)

        return (placements, set(widgets))

Classes

class GridLayout

Used to layout Widgets in to a grid.

Expand source code
class GridLayout(Layout):
    """Used to layout Widgets in to a grid."""

    name = "grid"

    def arrange(
        self, parent: Widget, children: list[Widget], size: Size
    ) -> ArrangeResult:
        styles = parent.styles
        row_scalars = styles.grid_rows or [Scalar.parse("1fr")]
        column_scalars = styles.grid_columns or [Scalar.parse("1fr")]
        gutter_horizontal = styles.grid_gutter_horizontal
        gutter_vertical = styles.grid_gutter_vertical
        table_size_columns = max(1, styles.grid_size_columns)
        table_size_rows = styles.grid_size_rows
        viewport = parent.screen.size

        def cell_coords(column_count: int) -> Iterable[tuple[int, int]]:
            """Iterate over table coordinates ad infinitum.

            Args:
                column_count (int): Number of columns

            """
            row = 0
            while True:
                for column in range(column_count):
                    yield (column, row)
                row += 1

        def widget_coords(
            column_start: int, row_start: int, columns: int, rows: int
        ) -> set[tuple[int, int]]:
            """Get coords occupied by a cell.

            Args:
                column_start (int): Start column.
                row_start (int): Start_row.
                columns (int): Number of columns.
                rows (int): Number of rows.

            Returns:
                set[tuple[int, int]]: Set of coords.
            """
            return {
                (column, row)
                for column in range(column_start, column_start + columns)
                for row in range(row_start, row_start + rows)
            }

        def repeat_scalars(scalars: Iterable[Scalar], count: int) -> list[Scalar]:
            """Repeat an iterable of scalars as many times as required to return
            a list of `count` values.

            Args:
                scalars (Iterable[T]): Iterable of values.
                count (int): Number of values to return.

            Returns:
                list[T]: A list of values.
            """
            limited_values = list(scalars)[:]
            while len(limited_values) < count:
                limited_values.extend(scalars)
            return limited_values[:count]

        cell_map: dict[tuple[int, int], tuple[Widget, bool]] = {}
        cell_size_map: dict[Widget, tuple[int, int, int, int]] = {}

        column_count = table_size_columns
        next_coord = iter(cell_coords(column_count)).__next__
        cell_coord = (0, 0)
        column = row = 0

        for child in children:
            child_styles = child.styles
            column_span = child_styles.column_span or 1
            row_span = child_styles.row_span or 1
            # Find a slot where this cell fits
            # A cell on a previous row may have a row span
            while True:
                column, row = cell_coord
                coords = widget_coords(column, row, column_span, row_span)
                if cell_map.keys().isdisjoint(coords):
                    for coord in coords:
                        cell_map[coord] = (child, coord == cell_coord)
                    cell_size_map[child] = (
                        column,
                        row,
                        column_span - 1,
                        row_span - 1,
                    )
                    break
                else:
                    cell_coord = next_coord()
                    continue
            cell_coord = next_coord()

        # Resolve columns / rows
        columns = resolve(
            repeat_scalars(column_scalars, table_size_columns),
            size.width,
            gutter_vertical,
            size,
            viewport,
        )
        rows = resolve(
            repeat_scalars(
                row_scalars, table_size_rows if table_size_rows else row + 1
            ),
            size.height,
            gutter_horizontal,
            size,
            viewport,
        )

        placements: list[WidgetPlacement] = []
        add_placement = placements.append
        fraction_unit = Fraction(1)
        widgets: list[Widget] = []
        add_widget = widgets.append
        max_column = len(columns) - 1
        max_row = len(rows) - 1
        margin = Spacing()
        for widget, (column, row, column_span, row_span) in cell_size_map.items():
            x = columns[column][0]
            if row > max_row:
                break
            y = rows[row][0]
            x2, cell_width = columns[min(max_column, column + column_span)]
            y2, cell_height = rows[min(max_row, row + row_span)]
            cell_size = Size(cell_width + x2 - x, cell_height + y2 - y)
            width, height, margin = widget._get_box_model(
                cell_size,
                viewport,
                fraction_unit,
            )
            region = (
                Region(x, y, int(width + margin.width), int(height + margin.height))
                .shrink(margin)
                .clip_size(cell_size)
            )
            add_placement(WidgetPlacement(region, margin, widget))
            add_widget(widget)

        return (placements, set(widgets))

Ancestors

  • textual._layout.Layout
  • abc.ABC

Class variables

var name : ClassVar[str]

Methods

def arrange(self, parent: Widget, children: list[Widget], size: Size) ‑> ArrangeResult

Generate a layout map that defines where on the screen the widgets will be drawn.

Args

parent : Widget
Parent widget.
size : Size
Size of container.

Returns

Iterable[WidgetPlacement]
An iterable of widget location
Expand source code
def arrange(
    self, parent: Widget, children: list[Widget], size: Size
) -> ArrangeResult:
    styles = parent.styles
    row_scalars = styles.grid_rows or [Scalar.parse("1fr")]
    column_scalars = styles.grid_columns or [Scalar.parse("1fr")]
    gutter_horizontal = styles.grid_gutter_horizontal
    gutter_vertical = styles.grid_gutter_vertical
    table_size_columns = max(1, styles.grid_size_columns)
    table_size_rows = styles.grid_size_rows
    viewport = parent.screen.size

    def cell_coords(column_count: int) -> Iterable[tuple[int, int]]:
        """Iterate over table coordinates ad infinitum.

        Args:
            column_count (int): Number of columns

        """
        row = 0
        while True:
            for column in range(column_count):
                yield (column, row)
            row += 1

    def widget_coords(
        column_start: int, row_start: int, columns: int, rows: int
    ) -> set[tuple[int, int]]:
        """Get coords occupied by a cell.

        Args:
            column_start (int): Start column.
            row_start (int): Start_row.
            columns (int): Number of columns.
            rows (int): Number of rows.

        Returns:
            set[tuple[int, int]]: Set of coords.
        """
        return {
            (column, row)
            for column in range(column_start, column_start + columns)
            for row in range(row_start, row_start + rows)
        }

    def repeat_scalars(scalars: Iterable[Scalar], count: int) -> list[Scalar]:
        """Repeat an iterable of scalars as many times as required to return
        a list of `count` values.

        Args:
            scalars (Iterable[T]): Iterable of values.
            count (int): Number of values to return.

        Returns:
            list[T]: A list of values.
        """
        limited_values = list(scalars)[:]
        while len(limited_values) < count:
            limited_values.extend(scalars)
        return limited_values[:count]

    cell_map: dict[tuple[int, int], tuple[Widget, bool]] = {}
    cell_size_map: dict[Widget, tuple[int, int, int, int]] = {}

    column_count = table_size_columns
    next_coord = iter(cell_coords(column_count)).__next__
    cell_coord = (0, 0)
    column = row = 0

    for child in children:
        child_styles = child.styles
        column_span = child_styles.column_span or 1
        row_span = child_styles.row_span or 1
        # Find a slot where this cell fits
        # A cell on a previous row may have a row span
        while True:
            column, row = cell_coord
            coords = widget_coords(column, row, column_span, row_span)
            if cell_map.keys().isdisjoint(coords):
                for coord in coords:
                    cell_map[coord] = (child, coord == cell_coord)
                cell_size_map[child] = (
                    column,
                    row,
                    column_span - 1,
                    row_span - 1,
                )
                break
            else:
                cell_coord = next_coord()
                continue
        cell_coord = next_coord()

    # Resolve columns / rows
    columns = resolve(
        repeat_scalars(column_scalars, table_size_columns),
        size.width,
        gutter_vertical,
        size,
        viewport,
    )
    rows = resolve(
        repeat_scalars(
            row_scalars, table_size_rows if table_size_rows else row + 1
        ),
        size.height,
        gutter_horizontal,
        size,
        viewport,
    )

    placements: list[WidgetPlacement] = []
    add_placement = placements.append
    fraction_unit = Fraction(1)
    widgets: list[Widget] = []
    add_widget = widgets.append
    max_column = len(columns) - 1
    max_row = len(rows) - 1
    margin = Spacing()
    for widget, (column, row, column_span, row_span) in cell_size_map.items():
        x = columns[column][0]
        if row > max_row:
            break
        y = rows[row][0]
        x2, cell_width = columns[min(max_column, column + column_span)]
        y2, cell_height = rows[min(max_row, row + row_span)]
        cell_size = Size(cell_width + x2 - x, cell_height + y2 - y)
        width, height, margin = widget._get_box_model(
            cell_size,
            viewport,
            fraction_unit,
        )
        region = (
            Region(x, y, int(width + margin.width), int(height + margin.height))
            .shrink(margin)
            .clip_size(cell_size)
        )
        add_placement(WidgetPlacement(region, margin, widget))
        add_widget(widget)

    return (placements, set(widgets))