From 34511529936a5df11927fa50f34ea48a70ac2c3e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 22 Jun 2022 11:03:04 +0100 Subject: [PATCH] cursor and hover --- src/textual/widget.py | 50 ++++++++----------- src/textual/widgets/_data_table.py | 80 ++++++++++++++++++++---------- 2 files changed, 76 insertions(+), 54 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index f845b5da3..948ff32d1 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -29,7 +29,7 @@ from ._context import active_app from ._types import Lines from .dom import DOMNode from ._layout import ArrangeResult -from .geometry import clamp, Offset, Region, Size +from .geometry import clamp, Offset, Region, Size, Spacing from .layouts.vertical import VerticalLayout from .message import Message from . import messages @@ -624,12 +624,15 @@ class Widget(DOMNode): return any(scrolls) - def scroll_to_region(self, region: Region, *, animate: bool = True) -> bool: + def scroll_to_region( + self, region: Region, *, spacing: Spacing | None = None, animate: bool = True + ) -> bool: """Scrolls a given region in to view. Args: region (Region): A region that should be visible. animate (bool, optional): Enable animation. Defaults to True. + spacing (Spacing): Space to subtract from the window region. Returns: bool: True if the window was scrolled. @@ -637,50 +640,41 @@ class Widget(DOMNode): scroll_x, scroll_y = self.scroll_offset width, height = self.region.size - container_region = Region(scroll_x, scroll_y, width, height) + window = Region(scroll_x, scroll_y, width, height) + if spacing is not None: + window = window.shrink(spacing) - if region in container_region: - # Widget is visible, nothing to do + if region in window: + # Widget is entirely visible, nothing to do return False - ( - container_left, - container_top, - container_right, - container_bottom, - ) = container_region.corners - ( - child_left, - child_top, - child_right, - child_bottom, - ) = region.corners + window_left, window_top, window_right, window_bottom = window.corners + left, top, right, bottom = region.corners - delta_x = 0 - delta_y = 0 + delta_x = delta_y = 0 if not ( - (container_right >= child_left > container_left) - and (container_right >= child_right > container_left) + (window_right > left >= window_left) + and (window_right > right >= window_left) ): delta_x = min( - child_left - container_left, - child_left - (container_right - region.width), + left - window_left, + left - (window_right - region.width), key=abs, ) if not ( - (container_bottom >= child_top > container_top) - and (container_bottom >= child_bottom > container_top) + (window_bottom > top >= window_top) + and (window_bottom > bottom >= window_top) ): delta_y = min( - child_top - container_top, - child_top - (container_bottom - region.height), + top - window_top, + top - (window_bottom - region.height), key=abs, ) scrolled = self.scroll_relative( - delta_x or None, delta_y or None, animate=abs(delta_y) != 1, duration=0.2 + delta_x or None, delta_y or None, animate=animate, duration=0.2 ) return scrolled diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 558f6d612..e9681a7d1 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -16,7 +16,7 @@ from .. import events from .._cache import LRUCache from .._segment_tools import line_crop from .._types import Lines -from ..geometry import clamp, Region, Size +from ..geometry import clamp, Region, Size, Spacing from ..reactive import Reactive from .._profile import timer from ..scroll_view import ScrollView @@ -103,8 +103,7 @@ class DataTable(ScrollView, Generic[CellType]): } DataTable > .datatable--highlight { - background: $secondary; - color: $text-secondary; + background: $primary 20%; } """ @@ -135,7 +134,7 @@ class DataTable(ScrollView, Generic[CellType]): self._row_render_cache = LRUCache(1000) self._cell_render_cache: LRUCache[tuple[int, int, Style], Lines] - self._cell_render_cache = LRUCache(10000) + self._cell_render_cache = LRUCache(1000) self._line_cache: LRUCache[tuple[int, int, int, int], list[Segment]] self._line_cache = LRUCache(1000) @@ -144,13 +143,15 @@ class DataTable(ScrollView, Generic[CellType]): show_header = Reactive(True) fixed_rows = Reactive(0) - fixed_columns = Reactive(0) + fixed_columns = Reactive(1) zebra_stripes = Reactive(False) header_height = Reactive(1) show_cursor = Reactive(True) cursor_type = Reactive(CELL) cursor_row = Reactive(0) - cursor_column = Reactive(0) + cursor_column = Reactive(1) + hover_row = Reactive(0) + hover_column = Reactive(0) def _clear_caches(self) -> None: self._row_render_cache.clear() @@ -188,7 +189,7 @@ class DataTable(ScrollView, Generic[CellType]): len(self._y_offsets) + (self.header_height if self.show_header else 0), ) - def _get_cursor_region(self, row_index: int, column_index: int) -> Region: + def _get_cell_region(self, row_index: int, column_index: int) -> Region: row = self.rows[row_index] x = sum(column.width for column in self.columns[:column_index]) width = self.columns[column_index].width @@ -256,6 +257,7 @@ class DataTable(ScrollView, Generic[CellType]): style: Style, width: int, cursor: bool = False, + hover: bool = False, ) -> Lines: """Render the given cell. @@ -268,9 +270,11 @@ class DataTable(ScrollView, Generic[CellType]): Returns: Lines: A list of segments per line. """ + if hover: + style += self.component_styles["datatable--highlight"].node.rich_style if cursor: style += self.component_styles["datatable--cursor"].node.rich_style - cell_key = (row_index, column_index, style) + cell_key = (row_index, column_index, style, cursor, hover) if cell_key not in self._cell_render_cache: style += Style.from_meta({"row": row_index, "column": column_index}) height = ( @@ -286,7 +290,12 @@ class DataTable(ScrollView, Generic[CellType]): return self._cell_render_cache[cell_key] def _render_row( - self, row_index: int, line_no: int, base_style: Style, cursor: int = -1 + self, + row_index: int, + line_no: int, + base_style: Style, + cursor_column: int = -1, + hover_column: int = -1, ) -> tuple[Lines, Lines]: """Render a row in to lines for each cell. @@ -299,7 +308,7 @@ class DataTable(ScrollView, Generic[CellType]): tuple[Lines, Lines]: Lines for fixed cells, and Lines for scrollable cells. """ - cache_key = (row_index, line_no, base_style) + cache_key = (row_index, line_no, base_style, cursor_column, hover_column) if cache_key in self._row_render_cache: return self._row_render_cache[cache_key] @@ -333,7 +342,8 @@ class DataTable(ScrollView, Generic[CellType]): column.index, row_style, column.width, - cursor=cursor == column.index, + cursor=cursor_column == column.index, + hover=hover_column == column.index, )[line_no] for column in self.columns ] @@ -373,20 +383,24 @@ class DataTable(ScrollView, Generic[CellType]): """ width = self.region.width + row_index, line_no = self._get_offsets(y) + cursor_column = ( + self.cursor_column + if (self.show_cursor and self.cursor_row == row_index) + else -1 + ) + hover_column = self.hover_column if (self.hover_row == row_index) else -1 - cache_key = (y, x1, x2, width) + cache_key = (y, x1, x2, width, cursor_column, hover_column) if cache_key in self._line_cache: return self._line_cache[cache_key] - row_index, line_no = self._get_offsets(y) - fixed, scrollable = self._render_row( row_index, line_no, base_style, - cursor=self.cursor_column - if (self.show_cursor and self.cursor_row == row_index) - else -1, + cursor_column=cursor_column, + hover_column=hover_column, ) fixed_width = sum(column.width for column in self.columns[: self.fixed_columns]) @@ -440,41 +454,55 @@ class DataTable(ScrollView, Generic[CellType]): return lines - def on_mouse_move(self, event): - print(self.get_style_at(event.x, event.y).meta) + def on_mouse_move(self, event: events.MouseMove): + meta = self.get_style_at(event.x, event.y).meta + self.hover_row = meta.get("row") + self.hover_column = meta.get("column") async def on_key(self, event) -> None: await self.dispatch_key(event) + def _get_cell_border(self) -> Spacing: + top = self.header_height if self.show_header else 0 + top += sum( + self.rows[row_index].height + for row_index in range(self.fixed_rows) + if row_index in self.rows + ) + left = sum(column.width for column in self.columns[: self.fixed_columns]) + return Spacing(top, 0, 0, left) + def _scroll_cursor_in_to_view(self) -> None: - region = self._get_cursor_region(self.cursor_row, self.cursor_column) - print("CURSOR", region) - self.scroll_to_region(region) + region = self._get_cell_region(self.cursor_row, self.cursor_column) + spacing = self._get_cell_border() + self.scroll_to_region(region, animate=False, spacing=spacing) + + def on_click(self, event: events.Click) -> None: + meta = self.get_style_at(event.x, event.y).meta + self.cursor_row = meta.get("row") + self.cursor_column = meta.get("column") + self._scroll_cursor_in_to_view() def key_down(self, event: events.Key): self.cursor_row += 1 - self._clear_caches() event.stop() event.prevent_default() self._scroll_cursor_in_to_view() def key_up(self, event: events.Key): self.cursor_row -= 1 - self._clear_caches() event.stop() event.prevent_default() self._scroll_cursor_in_to_view() def key_right(self, event: events.Key): self.cursor_column += 1 - self._clear_caches() event.stop() event.prevent_default() self._scroll_cursor_in_to_view() def key_left(self, event: events.Key): self.cursor_column -= 1 - self._clear_caches() event.stop() event.prevent_default() self._scroll_cursor_in_to_view()