Skip to content

Commit

Permalink
Merge pull request #3443 from Textualize/data-table-cell-padding
Browse files Browse the repository at this point in the history
Data table cell padding
  • Loading branch information
rodrigogiraoserrao authored Oct 3, 2023
2 parents f4f83ee + 1936f10 commit 571ea7c
Show file tree
Hide file tree
Showing 6 changed files with 517 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added

- `OutOfBounds` exception to be raised by `Pilot` https://github.com/Textualize/textual/pull/3360
- Reactive `cell_padding` (and respective parameter) to define horizontal cell padding in data table columns https://github.com/Textualize/textual/issues/3435
- Added `Input.clear` method https://github.com/Textualize/textual/pull/3430
- Added `TextArea.SelectionChanged` and `TextArea.Changed` messages https://github.com/Textualize/textual/pull/3442

Expand Down
120 changes: 95 additions & 25 deletions src/textual/widgets/_data_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"""The valid types of cursors for [`DataTable.cursor_type`][textual.widgets.DataTable.cursor_type]."""
CellType = TypeVar("CellType")

CELL_X_PADDING = 2
_DEFAULT_CELL_X_PADDING = 1
"""Default padding to use on each side of a column in the data table."""


class CellDoesNotExist(Exception):
Expand Down Expand Up @@ -170,14 +171,18 @@ class Column:
content_width: int = 0
auto_width: bool = False

@property
def render_width(self) -> int:
"""Width in cells, required to render a column."""
# +2 is to account for space padding either side of the cell
if self.auto_width:
return self.content_width + CELL_X_PADDING
else:
return self.width + CELL_X_PADDING
def get_render_width(self, data_table: DataTable[Any]) -> int:
"""Width, in cells, required to render the column with padding included.
Args:
data_table: The data table where the column will be rendered.
Returns:
The width, in cells, required to render the column with padding included.
"""
return 2 * data_table.cell_padding + (
self.content_width if self.auto_width else self.width
)


@dataclass
Expand Down Expand Up @@ -309,6 +314,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
show_cursor = Reactive(True)
cursor_type: Reactive[CursorType] = Reactive[CursorType]("cell")
"""The type of the cursor of the `DataTable`."""
cell_padding = Reactive(_DEFAULT_CELL_X_PADDING)
"""Horizontal padding between cells, applied on each side of each cell."""

cursor_coordinate: Reactive[Coordinate] = Reactive(
Coordinate(0, 0), repaint=False, always_update=True
Expand All @@ -323,6 +330,9 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
)
"""The coordinate of the `DataTable` that is being hovered."""

def watch_cell_padding(self) -> None:
self._clear_caches()

class CellHighlighted(Message):
"""Posted when the cursor moves to highlight a new cell.
Expand Down Expand Up @@ -584,11 +594,43 @@ def __init__(
cursor_foreground_priority: Literal["renderable", "css"] = "css",
cursor_background_priority: Literal["renderable", "css"] = "renderable",
cursor_type: CursorType = "cell",
cell_padding: int = _DEFAULT_CELL_X_PADDING,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
) -> None:
"""Initialises a widget to display tabular data.
Args:
show_header: Whether the table header should be visible or not.
show_row_labels: Whether the row labels should be shown or not.
fixed_rows: The number of rows, counting from the top, that should be fixed
and still visible when the user scrolls down.
fixed_columns: The number of columns, counting from the left, that should be
fixed and still visible when the user scrolls right.
zebra_stripes: Enables or disables a zebra effect applied to the background
color of the rows of the table, where alternate colors are styled
differently to improve the readability of the table.
header_height: The height, in number of cells, of the data table header.
show_cursor: Whether the cursor should be visible when navigating the data
table or not.
cursor_foreground_priority: If the data associated with a cell is an
arbitrary renderable with a set foreground color, this determines whether
that color is prioritised over the cursor component class or not.
cursor_background_priority: If the data associated with a cell is an
arbitrary renderable with a set background color, this determines whether
that color is prioritesed over the cursor component class or not.
cursor_type: The type of cursor to be used when navigating the data table
with the keyboard.
cell_padding: The number of cells added on each side of each column. Setting
this value to zero will likely make your table very heard to read.
name: The name of the widget.
id: The ID of the widget in the DOM.
classes: The CSS classes for the widget.
disabled: Whether the widget is disabled or not.
"""

super().__init__(name=name, id=id, classes=classes, disabled=disabled)
self._data: dict[RowKey, dict[ColumnKey, CellType]] = {}
"""Contains the cells of the table, indexed by row key and column key.
Expand Down Expand Up @@ -673,6 +715,8 @@ def __init__(
in the event where a cell contains a renderable with a background color."""
self.cursor_type = cursor_type
"""The type of cursor of the `DataTable`."""
self.cell_padding = cell_padding
"""Horizontal padding between cells, applied on each side of each cell."""

@property
def hover_row(self) -> int:
Expand Down Expand Up @@ -996,7 +1040,7 @@ def watch_show_header(self, show: bool) -> None:

def watch_show_row_labels(self, show: bool) -> None:
width, height = self.virtual_size
column_width = self._label_column.render_width
column_width = self._label_column.get_render_width(self)
width_change = column_width if show else -column_width
self.virtual_size = Size(width + width_change, height)
self._scroll_cursor_into_view()
Expand All @@ -1011,6 +1055,19 @@ def watch_fixed_columns(self) -> None:
def watch_zebra_stripes(self) -> None:
self._clear_caches()

def validate_cell_padding(self, cell_padding: int) -> int:
return max(cell_padding, 0)

def watch_cell_padding(self, old_padding: int, new_padding: int) -> None:
# A single side of a single cell will have its width changed by (new - old),
# so the total width change is double that per column, times the number of
# columns for the whole data table.
width_change = 2 * (new_padding - old_padding) * len(self.columns)
width, height = self.virtual_size
self.virtual_size = Size(width + width_change, height)
self._scroll_cursor_into_view()
self._clear_caches()

def watch_hover_coordinate(self, old: Coordinate, value: Coordinate) -> None:
self.refresh_coordinate(old)
self.refresh_coordinate(value)
Expand Down Expand Up @@ -1164,7 +1221,11 @@ def _highlight_cursor(self) -> None:
@property
def _row_label_column_width(self) -> int:
"""The render width of the column containing row labels"""
return self._label_column.render_width if self._should_render_row_labels else 0
return (
self._label_column.get_render_width(self)
if self._should_render_row_labels
else 0
)

def _update_column_widths(self, updated_cells: set[CellKey]) -> None:
"""Update the widths of the columns based on the newly updated cell widths."""
Expand Down Expand Up @@ -1259,7 +1320,7 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
row_index,
column_index,
style,
column.render_width,
column.get_render_width(self),
cursor=should_highlight(
cursor_location, cell_location, cursor_type
),
Expand All @@ -1269,7 +1330,7 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
)
cell_height = len(rendered_cell)
rendered_cells.append(
(rendered_cell, cell_height, column.render_width)
(rendered_cell, cell_height, column.get_render_width(self))
)
height = max(height, cell_height)

Expand All @@ -1286,7 +1347,9 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
]
)

data_cells_width = sum(column.render_width for column in self.columns.values())
data_cells_width = sum(
column.get_render_width(self) for column in self.columns.values()
)
total_width = data_cells_width + self._row_label_column_width
header_height = self.header_height if self.show_header else 0
self.virtual_size = Size(
Expand All @@ -1306,11 +1369,14 @@ def _get_cell_region(self, coordinate: Coordinate) -> Region:
# The x-coordinate of a cell is the sum of widths of the data cells to the left
# plus the width of the render width of the longest row label.
x = (
sum(column.render_width for column in self.ordered_columns[:column_index])
sum(
column.get_render_width(self)
for column in self.ordered_columns[:column_index]
)
+ self._row_label_column_width
)
column_key = self._column_locations.get_key(column_index)
width = self.columns[column_key].render_width
width = self.columns[column_key].get_render_width(self)
height = row.height
y = sum(ordered_row.height for ordered_row in self.ordered_rows[:row_index])
if self.show_header:
Expand All @@ -1327,7 +1393,7 @@ def _get_row_region(self, row_index: int) -> Region:
row_key = self._row_locations.get_key(row_index)
row = rows[row_key]
row_width = (
sum(column.render_width for column in self.columns.values())
sum(column.get_render_width(self) for column in self.columns.values())
+ self._row_label_column_width
)
y = sum(ordered_row.height for ordered_row in self.ordered_rows[:row_index])
Expand All @@ -1343,11 +1409,14 @@ def _get_column_region(self, column_index: int) -> Region:

columns = self.columns
x = (
sum(column.render_width for column in self.ordered_columns[:column_index])
sum(
column.get_render_width(self)
for column in self.ordered_columns[:column_index]
)
+ self._row_label_column_width
)
column_key = self._column_locations.get_key(column_index)
width = columns[column_key].render_width
width = columns[column_key].get_render_width(self)
header_height = self.header_height if self.show_header else 0
height = self._total_row_height + header_height
full_column_region = Region(x, 0, width, height)
Expand Down Expand Up @@ -1881,7 +1950,7 @@ def _render_cell(
)
lines = self.app.console.render_lines(
Styled(
Padding(cell, (0, 1)),
Padding(cell, (0, self.cell_padding)),
pre_style=base_style + component_style,
post_style=post_style,
),
Expand Down Expand Up @@ -2030,7 +2099,7 @@ def _render_line_in_row(
row_index,
column_index,
fixed_style,
column.render_width,
column.get_render_width(self),
cursor=should_highlight(
cursor_location, cell_location, cursor_type
),
Expand All @@ -2047,7 +2116,7 @@ def _render_line_in_row(
row_index,
column_index,
row_style,
column.render_width,
column.get_render_width(self),
cursor=should_highlight(cursor_location, cell_location, cursor_type),
hover=should_highlight(hover_location, cell_location, cursor_type),
)[line_no]
Expand All @@ -2057,7 +2126,7 @@ def _render_line_in_row(
widget_width = self.size.width
table_width = (
sum(
column.render_width
column.get_render_width(self)
for column in self.ordered_columns[self.fixed_columns :]
)
+ self._row_label_column_width
Expand Down Expand Up @@ -2141,7 +2210,8 @@ def _render_line(self, y: int, x1: int, x2: int, base_style: Style) -> Strip:
hover_location=self.hover_coordinate,
)
fixed_width = sum(
column.render_width for column in self.ordered_columns[: self.fixed_columns]
column.get_render_width(self)
for column in self.ordered_columns[: self.fixed_columns]
)

fixed_line: list[Segment] = list(chain.from_iterable(fixed)) if fixed else []
Expand Down Expand Up @@ -2260,7 +2330,7 @@ def _get_fixed_offset(self) -> Spacing:
top += sum(row.height for row in self.ordered_rows[: self.fixed_rows])
left = (
sum(
column.render_width
column.get_render_width(self)
for column in self.ordered_columns[: self.fixed_columns]
)
+ self._row_label_column_width
Expand Down
Loading

0 comments on commit 571ea7c

Please sign in to comment.