mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
cursor and hover
This commit is contained in:
@@ -29,7 +29,7 @@ from ._context import active_app
|
|||||||
from ._types import Lines
|
from ._types import Lines
|
||||||
from .dom import DOMNode
|
from .dom import DOMNode
|
||||||
from ._layout import ArrangeResult
|
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 .layouts.vertical import VerticalLayout
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from . import messages
|
from . import messages
|
||||||
@@ -624,12 +624,15 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
return any(scrolls)
|
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.
|
"""Scrolls a given region in to view.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
region (Region): A region that should be visible.
|
region (Region): A region that should be visible.
|
||||||
animate (bool, optional): Enable animation. Defaults to True.
|
animate (bool, optional): Enable animation. Defaults to True.
|
||||||
|
spacing (Spacing): Space to subtract from the window region.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the window was scrolled.
|
bool: True if the window was scrolled.
|
||||||
@@ -637,50 +640,41 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
scroll_x, scroll_y = self.scroll_offset
|
scroll_x, scroll_y = self.scroll_offset
|
||||||
width, height = self.region.size
|
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:
|
if region in window:
|
||||||
# Widget is visible, nothing to do
|
# Widget is entirely visible, nothing to do
|
||||||
return False
|
return False
|
||||||
|
|
||||||
(
|
window_left, window_top, window_right, window_bottom = window.corners
|
||||||
container_left,
|
left, top, right, bottom = region.corners
|
||||||
container_top,
|
|
||||||
container_right,
|
|
||||||
container_bottom,
|
|
||||||
) = container_region.corners
|
|
||||||
(
|
|
||||||
child_left,
|
|
||||||
child_top,
|
|
||||||
child_right,
|
|
||||||
child_bottom,
|
|
||||||
) = region.corners
|
|
||||||
|
|
||||||
delta_x = 0
|
delta_x = delta_y = 0
|
||||||
delta_y = 0
|
|
||||||
|
|
||||||
if not (
|
if not (
|
||||||
(container_right >= child_left > container_left)
|
(window_right > left >= window_left)
|
||||||
and (container_right >= child_right > container_left)
|
and (window_right > right >= window_left)
|
||||||
):
|
):
|
||||||
delta_x = min(
|
delta_x = min(
|
||||||
child_left - container_left,
|
left - window_left,
|
||||||
child_left - (container_right - region.width),
|
left - (window_right - region.width),
|
||||||
key=abs,
|
key=abs,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not (
|
if not (
|
||||||
(container_bottom >= child_top > container_top)
|
(window_bottom > top >= window_top)
|
||||||
and (container_bottom >= child_bottom > container_top)
|
and (window_bottom > bottom >= window_top)
|
||||||
):
|
):
|
||||||
delta_y = min(
|
delta_y = min(
|
||||||
child_top - container_top,
|
top - window_top,
|
||||||
child_top - (container_bottom - region.height),
|
top - (window_bottom - region.height),
|
||||||
key=abs,
|
key=abs,
|
||||||
)
|
)
|
||||||
|
|
||||||
scrolled = self.scroll_relative(
|
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
|
return scrolled
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from .. import events
|
|||||||
from .._cache import LRUCache
|
from .._cache import LRUCache
|
||||||
from .._segment_tools import line_crop
|
from .._segment_tools import line_crop
|
||||||
from .._types import Lines
|
from .._types import Lines
|
||||||
from ..geometry import clamp, Region, Size
|
from ..geometry import clamp, Region, Size, Spacing
|
||||||
from ..reactive import Reactive
|
from ..reactive import Reactive
|
||||||
from .._profile import timer
|
from .._profile import timer
|
||||||
from ..scroll_view import ScrollView
|
from ..scroll_view import ScrollView
|
||||||
@@ -103,8 +103,7 @@ class DataTable(ScrollView, Generic[CellType]):
|
|||||||
}
|
}
|
||||||
|
|
||||||
DataTable > .datatable--highlight {
|
DataTable > .datatable--highlight {
|
||||||
background: $secondary;
|
background: $primary 20%;
|
||||||
color: $text-secondary;
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -135,7 +134,7 @@ class DataTable(ScrollView, Generic[CellType]):
|
|||||||
self._row_render_cache = LRUCache(1000)
|
self._row_render_cache = LRUCache(1000)
|
||||||
|
|
||||||
self._cell_render_cache: LRUCache[tuple[int, int, Style], Lines]
|
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[tuple[int, int, int, int], list[Segment]]
|
||||||
self._line_cache = LRUCache(1000)
|
self._line_cache = LRUCache(1000)
|
||||||
@@ -144,13 +143,15 @@ class DataTable(ScrollView, Generic[CellType]):
|
|||||||
|
|
||||||
show_header = Reactive(True)
|
show_header = Reactive(True)
|
||||||
fixed_rows = Reactive(0)
|
fixed_rows = Reactive(0)
|
||||||
fixed_columns = Reactive(0)
|
fixed_columns = Reactive(1)
|
||||||
zebra_stripes = Reactive(False)
|
zebra_stripes = Reactive(False)
|
||||||
header_height = Reactive(1)
|
header_height = Reactive(1)
|
||||||
show_cursor = Reactive(True)
|
show_cursor = Reactive(True)
|
||||||
cursor_type = Reactive(CELL)
|
cursor_type = Reactive(CELL)
|
||||||
cursor_row = Reactive(0)
|
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:
|
def _clear_caches(self) -> None:
|
||||||
self._row_render_cache.clear()
|
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),
|
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]
|
row = self.rows[row_index]
|
||||||
x = sum(column.width for column in self.columns[:column_index])
|
x = sum(column.width for column in self.columns[:column_index])
|
||||||
width = self.columns[column_index].width
|
width = self.columns[column_index].width
|
||||||
@@ -256,6 +257,7 @@ class DataTable(ScrollView, Generic[CellType]):
|
|||||||
style: Style,
|
style: Style,
|
||||||
width: int,
|
width: int,
|
||||||
cursor: bool = False,
|
cursor: bool = False,
|
||||||
|
hover: bool = False,
|
||||||
) -> Lines:
|
) -> Lines:
|
||||||
"""Render the given cell.
|
"""Render the given cell.
|
||||||
|
|
||||||
@@ -268,9 +270,11 @@ class DataTable(ScrollView, Generic[CellType]):
|
|||||||
Returns:
|
Returns:
|
||||||
Lines: A list of segments per line.
|
Lines: A list of segments per line.
|
||||||
"""
|
"""
|
||||||
|
if hover:
|
||||||
|
style += self.component_styles["datatable--highlight"].node.rich_style
|
||||||
if cursor:
|
if cursor:
|
||||||
style += self.component_styles["datatable--cursor"].node.rich_style
|
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:
|
if cell_key not in self._cell_render_cache:
|
||||||
style += Style.from_meta({"row": row_index, "column": column_index})
|
style += Style.from_meta({"row": row_index, "column": column_index})
|
||||||
height = (
|
height = (
|
||||||
@@ -286,7 +290,12 @@ class DataTable(ScrollView, Generic[CellType]):
|
|||||||
return self._cell_render_cache[cell_key]
|
return self._cell_render_cache[cell_key]
|
||||||
|
|
||||||
def _render_row(
|
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]:
|
) -> tuple[Lines, Lines]:
|
||||||
"""Render a row in to lines for each cell.
|
"""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.
|
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:
|
if cache_key in self._row_render_cache:
|
||||||
return self._row_render_cache[cache_key]
|
return self._row_render_cache[cache_key]
|
||||||
@@ -333,7 +342,8 @@ class DataTable(ScrollView, Generic[CellType]):
|
|||||||
column.index,
|
column.index,
|
||||||
row_style,
|
row_style,
|
||||||
column.width,
|
column.width,
|
||||||
cursor=cursor == column.index,
|
cursor=cursor_column == column.index,
|
||||||
|
hover=hover_column == column.index,
|
||||||
)[line_no]
|
)[line_no]
|
||||||
for column in self.columns
|
for column in self.columns
|
||||||
]
|
]
|
||||||
@@ -373,20 +383,24 @@ class DataTable(ScrollView, Generic[CellType]):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
width = self.region.width
|
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:
|
if cache_key in self._line_cache:
|
||||||
return self._line_cache[cache_key]
|
return self._line_cache[cache_key]
|
||||||
|
|
||||||
row_index, line_no = self._get_offsets(y)
|
|
||||||
|
|
||||||
fixed, scrollable = self._render_row(
|
fixed, scrollable = self._render_row(
|
||||||
row_index,
|
row_index,
|
||||||
line_no,
|
line_no,
|
||||||
base_style,
|
base_style,
|
||||||
cursor=self.cursor_column
|
cursor_column=cursor_column,
|
||||||
if (self.show_cursor and self.cursor_row == row_index)
|
hover_column=hover_column,
|
||||||
else -1,
|
|
||||||
)
|
)
|
||||||
fixed_width = sum(column.width for column in self.columns[: self.fixed_columns])
|
fixed_width = sum(column.width for column in self.columns[: self.fixed_columns])
|
||||||
|
|
||||||
@@ -440,41 +454,55 @@ class DataTable(ScrollView, Generic[CellType]):
|
|||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def on_mouse_move(self, event):
|
def on_mouse_move(self, event: events.MouseMove):
|
||||||
print(self.get_style_at(event.x, event.y).meta)
|
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:
|
async def on_key(self, event) -> None:
|
||||||
await self.dispatch_key(event)
|
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:
|
def _scroll_cursor_in_to_view(self) -> None:
|
||||||
region = self._get_cursor_region(self.cursor_row, self.cursor_column)
|
region = self._get_cell_region(self.cursor_row, self.cursor_column)
|
||||||
print("CURSOR", region)
|
spacing = self._get_cell_border()
|
||||||
self.scroll_to_region(region)
|
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):
|
def key_down(self, event: events.Key):
|
||||||
self.cursor_row += 1
|
self.cursor_row += 1
|
||||||
self._clear_caches()
|
|
||||||
event.stop()
|
event.stop()
|
||||||
event.prevent_default()
|
event.prevent_default()
|
||||||
self._scroll_cursor_in_to_view()
|
self._scroll_cursor_in_to_view()
|
||||||
|
|
||||||
def key_up(self, event: events.Key):
|
def key_up(self, event: events.Key):
|
||||||
self.cursor_row -= 1
|
self.cursor_row -= 1
|
||||||
self._clear_caches()
|
|
||||||
event.stop()
|
event.stop()
|
||||||
event.prevent_default()
|
event.prevent_default()
|
||||||
self._scroll_cursor_in_to_view()
|
self._scroll_cursor_in_to_view()
|
||||||
|
|
||||||
def key_right(self, event: events.Key):
|
def key_right(self, event: events.Key):
|
||||||
self.cursor_column += 1
|
self.cursor_column += 1
|
||||||
self._clear_caches()
|
|
||||||
event.stop()
|
event.stop()
|
||||||
event.prevent_default()
|
event.prevent_default()
|
||||||
self._scroll_cursor_in_to_view()
|
self._scroll_cursor_in_to_view()
|
||||||
|
|
||||||
def key_left(self, event: events.Key):
|
def key_left(self, event: events.Key):
|
||||||
self.cursor_column -= 1
|
self.cursor_column -= 1
|
||||||
self._clear_caches()
|
|
||||||
event.stop()
|
event.stop()
|
||||||
event.prevent_default()
|
event.prevent_default()
|
||||||
self._scroll_cursor_in_to_view()
|
self._scroll_cursor_in_to_view()
|
||||||
|
|||||||
Reference in New Issue
Block a user