Skip to content

Commit

Permalink
render selection
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Dec 18, 2024
1 parent 88b2638 commit 82d0025
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 20 deletions.
22 changes: 22 additions & 0 deletions src/textual/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
from textual._loop import loop_last
from textual.color import Color
from textual.css.types import TextAlign
from textual.geometry import Offset
from textual.selection import Selection
from textual.strip import Strip
from textual.visual import Style, Visual

Expand Down Expand Up @@ -270,7 +272,9 @@ def render_strips(
),
no_wrap=False,
tab_size=8,
selection=widget.selection,
)

if height is not None:
lines = lines[:height]

Expand Down Expand Up @@ -948,13 +952,30 @@ def wrap(
overflow: OverflowMethod = "fold",
no_wrap: bool = False,
tab_size: int = 8,
selection: Selection | None = None,
) -> list[Content]:
lines: list[Content] = []

selection = Selection(Offset(10, 0), Offset(20, 1))

if selection is not None:
get_span = selection.get_span
else:

def get_span(y: int) -> tuple[int, int] | None:
return None

for line_no, line in enumerate(self.split(allow_blank=True)):
if "\t" in line._text:
line = line.expand_tabs(tab_size)
line = line.stylize(Style.from_meta({"offset": (line_no, 0)}))

if (span := get_span(line_no)) is not None:
start, end = span
if end == -1:
end = len(line.plain)
line = line.stylize(Style(reverse=True), start, end)

if no_wrap:
new_lines = [line]
else:
Expand All @@ -964,6 +985,7 @@ def wrap(
new_lines = _align_lines(new_lines, width, align=align, overflow=overflow)
new_lines = [line.truncate(width, overflow=overflow) for line in new_lines]
lines.extend(new_lines)

return lines

def highlight_regex(
Expand Down
3 changes: 2 additions & 1 deletion src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@
from textual.reactive import Reactive, var
from textual.renderables.background_screen import BackgroundScreen
from textual.renderables.blank import Blank
from textual.selection import Selection
from textual.signal import Signal
from textual.timer import Timer
from textual.widget import Selection, Widget
from textual.widget import Widget
from textual.widgets import Tooltip
from textual.widgets._toast import ToastRack

Expand Down
49 changes: 49 additions & 0 deletions src/textual/selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from typing import NamedTuple

from textual.geometry import Offset


class Selection(NamedTuple):
"""A selected range of lines."""

start: Offset | None
"""Offset or None for `start`."""
end: Offset | None
"""Offset or None for `end`."""

def get_span(self, y: int) -> tuple[int, int] | None:
"""Get the selected span in a given line.
Args:
y: Offset of the line.
Returns:
A tuple of x start and end offset, or None for no selection.
"""
start, end = self
if start is None and end is None:
# Selection covers everything
return 0, -1

if start is not None and end is not None:
if y < start.y or y > end.y:
# Outside
return None
if y == start.y == end.y:
# Same line
return start.x, end.x
if y == end.y:
# Last line
return 0, end.x
# Remaining lines
return start.x, -1

if start is None and end is not None:
if y == end.y:
return 0, end.x
return 0, -1

if end is None and start is not None:
if y == start.y:
return start.x, -1
return 0, -1
7 changes: 7 additions & 0 deletions src/textual/visual.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ class Style:
dim: bool | None = None
italic: bool | None = None
underline: bool | None = None
reverse: bool | None = None
strike: bool | None = None
link: str | None = None
_meta: bytes | None = None
Expand All @@ -122,6 +123,7 @@ def __rich_repr__(self) -> rich.repr.Result:
yield "dim", self.dim, None
yield "italic", self.italic, None
yield "underline", self.underline, None
yield "reverse", self.reverse, None
yield "strike", self.strike, None
if self._meta is not None:
yield "meta", self.meta
Expand All @@ -137,6 +139,7 @@ def __add__(self, other: object) -> Style:
self.dim if other.dim is None else other.dim,
self.italic if other.italic is None else other.italic,
self.underline if other.underline is None else other.underline,
self.reverse if other.reverse is None else other.reverse,
self.strike if other.strike is None else other.strike,
self.link if other.link is None else other.link,
self._meta if other._meta is None else other._meta,
Expand All @@ -163,6 +166,7 @@ def from_rich_style(
dim=rich_style.dim,
italic=rich_style.italic,
underline=rich_style.underline,
reverse=rich_style.reverse,
strike=rich_style.strike,
)

Expand All @@ -186,6 +190,7 @@ def from_styles(cls, styles: StylesBase) -> Style:
dim=text_style.italic,
italic=text_style.italic,
underline=text_style.underline,
reverse=text_style.reverse,
strike=text_style.strike,
auto_color=styles.auto_color,
)
Expand All @@ -208,6 +213,7 @@ def rich_style(self) -> RichStyle:
dim=self.dim,
italic=self.italic,
underline=self.underline,
reverse=self.reverse,
strike=self.strike,
link=self.link,
meta=self.meta,
Expand All @@ -219,6 +225,7 @@ def without_color(self) -> Style:
bold=self.bold,
dim=self.dim,
italic=self.italic,
reverse=self.reverse,
strike=self.strike,
link=self.link,
_meta=self._meta,
Expand Down
24 changes: 5 additions & 19 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
from textual.reactive import Reactive
from textual.renderables.blank import Blank
from textual.rlock import RLock
from textual.selection import Selection
from textual.strip import Strip
from textual.visual import Style as VisualStyle
from textual.visual import Visual, visualize
Expand All @@ -109,29 +110,10 @@
}


_NULL_STYLE = Style()
_MOUSE_EVENTS_DISALLOW_IF_DISABLED = (events.MouseEvent, events.Enter, events.Leave)
_MOUSE_EVENTS_ALLOW_IF_DISABLED = (events.MouseScrollDown, events.MouseScrollUp)


class Selection(NamedTuple):
"""A selected range of lines."""

start: Offset | None
end: Offset | None

def get_selection(self, line_no: int) -> tuple[int, int] | None:
start, end = self
if start is None and end is None:
# Selection covers everything
return 0, -1
if start is not None and end is not None:
if line_no < start.y or line_no >= end.y:
return None
# TODO
return None


@rich.repr.auto
class AwaitMount:
"""An *optional* awaitable returned by [mount][textual.widget.Widget.mount] and [mount_all][textual.widget.Widget.mount_all].
Expand Down Expand Up @@ -654,6 +636,10 @@ def _render_widget(self) -> Widget:
# Will return the "cover widget" if one is set, otherwise self.
return self._cover_widget if self._cover_widget is not None else self

@property
def selection(self) -> Selection | None:
return self.screen.selections.get(self, None)

def _cover(self, widget: Widget) -> None:
"""Set a widget used to replace the visuals of this widget (used for loading indicator).
Expand Down

0 comments on commit 82d0025

Please sign in to comment.