Skip to content

Commit

Permalink
content line refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Dec 28, 2024
1 parent 6734f44 commit c242510
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 76 deletions.
176 changes: 117 additions & 59 deletions src/textual/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,56 @@ def get_optimal_width(self, container_width: int) -> int:
lines = self.without_spans.split("\n")
return max(line.cell_length for line in lines)

def wrap(
self,
width: int,
align: TextAlign = "left",
overflow: OverflowMethod = "fold",
no_wrap: bool = False,
tab_size: int = 8,
selection: Selection | None = None,
selection_style: Style | None = None,
) -> list[ContentLine]:
# lines: list[Content] = []
output_lines: list[ContentLine] = []

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

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

for y, line in enumerate(self.split(allow_blank=True)):
if selection_style is not None and (span := get_span(y)) is not None:
start, end = span
if end == -1:
end = len(line.plain)
line = line.stylize(selection_style, start, end)

content_line = ContentLine(line.expand_tabs(tab_size), width, y=y)

if no_wrap:
new_lines = [content_line]
else:
offsets = divide_line(line.plain, width, fold=overflow == "fold")
divided_lines = content_line.content.divide(offsets)
new_lines = [
ContentLine(content.rstrip_end(width), width, offset, y)
for content, offset in zip(divided_lines, [0, *offsets])
]

# new_lines = [_line.rstrip_end(width) for _line in new_lines]
# if align in ("left", "justify"):
# 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)
output_lines.extend(new_lines)

return output_lines

def render_strips(
self,
widget: Widget,
Expand Down Expand Up @@ -288,13 +338,11 @@ def render_strips(
if height is not None:
lines = lines[:height]

strip_lines = [
Strip(line.render_segments(style), line.cell_length) for line in lines
]

if align in ("left", "right", "center"):
strip_lines = [strip.text_align(width, align) for strip in strip_lines]

# strip_lines = [
# Strip(line.content.render_segments(style), line.content.cell_length)
# for line in lines
# ]
strip_lines = [line.to_strip(style) for line in lines]
return strip_lines

def get_height(self, width: int) -> int:
Expand Down Expand Up @@ -550,11 +598,13 @@ def pad_left(self, count: int, character: str = " ") -> Content:
_Span(start + count, end + count, style)
for start, end, style in self._spans
]
return Content(
content = Content(
text,
spans,
None if self._cell_length is None else self._cell_length + count,
)
return content

return self

def extend_right(self, count: int, character: str = " ") -> Content:
Expand Down Expand Up @@ -780,7 +830,6 @@ def get_current_style() -> Style:
def render_segments(self, base_style: Style, end: str = "") -> list[Segment]:
_Segment = Segment
render = list(self.render(base_style, end))

segments = [_Segment(text, style.get_rich_style()) for text, style in render]
return segments

Expand Down Expand Up @@ -965,56 +1014,6 @@ def expand_tabs(self, tab_size: int = 8) -> Content:
content = Content("").join(new_text)
return content

def wrap(
self,
width: int,
align: TextAlign = "center",
overflow: OverflowMethod = "fold",
no_wrap: bool = False,
tab_size: int = 8,
selection: Selection | None = None,
selection_style: Style | None = None,
) -> list[Content]:
lines: list[Content] = []

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

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

for y, line in enumerate(self.split(allow_blank=True)):
line = line.stylize(Style(y=y))
if "\t" in line._text:
line = line.expand_tabs(tab_size)

if selection_style is not None and (span := get_span(y)) is not None:
start, end = span
if end == -1:
end = len(line.plain)
line = line.stylize(selection_style, start, end)

if no_wrap:
new_lines = [line]
else:
offsets = divide_line(line._text, width, fold=overflow == "fold")
new_lines = line.divide(offsets)
new_lines = [
line.stylize(Style(x=offset))
for offset, line in zip([0, *offsets], lines)
]

new_lines = [line.rstrip_end(width) for line in new_lines]
if align in ("left", "justify"):
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(
self,
re_highlight: re.Pattern[str] | str,
Expand All @@ -1033,6 +1032,65 @@ def highlight_regex(
return Content(self._text, spans)


class ContentLine:
def __init__(self, content: Content, width: int, x: int = 0, y: int = 0) -> None:
self.content = content
self.width = width
self.x = x
self.y = y
self.pad_left = 0
self.pad_right = 0
self.highlight_style: Style | None = None
self.highlight_range: tuple[int | None, int | None] | None = None

@property
def plain(self) -> str:
return self.content.plain

def center(self, width: int):
excess_space = width - self.content.cell_length
self.pad_left = excess_space // 2
self.pad_right = excess_space - self.pad_left

def left(self, width: int) -> None:
self.pad_left = 0
self.right_left = width - self.content.cell_length

def right(self, width: int) -> None:
self.pad_left = 0
self.pad_right = width - self.content.cell_length

def highlight(self, style: Style, start: int | None, end: int | None) -> None:
self.highlight_style = style
self.highlight_range = (start, end)

def to_strip(self, style: Style) -> Strip:
self.left(self.width)
content = self.content
if self.highlight_style is not None and self.highlight_range is not None:
start, end = self.highlight_range
content = content.stylize(self.highlight_style, start, end)
content = content.truncate(self.width)
_Segment = Segment
base_rich_style = style.rich_style
x = self.x
y = self.y
segments: list[Segment] = (
[Segment(" " * self.pad_left, base_rich_style)] if self.pad_left else []
)
add_segment = segments.append
for text, text_style in content.render(style, end=""):
add_segment(
_Segment(text, (style + text_style).rich_style_with_offset(x, y))
)
x += len(text)

if self.pad_right:
segments.append(Segment(" " * self.pad_right, base_rich_style))
strip = Strip(segments, content.cell_length + self.pad_left + self.pad_right)
return strip


if __name__ == "__main__":
from rich import print

Expand Down
5 changes: 4 additions & 1 deletion src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ class Screen(Generic[ScreenResultType], Widget):
_selecting = var(False)

_select_start: Reactive[tuple[Widget, Offset, Offset] | None] = Reactive(None)
"""Tuple of (widget, screen offset, text offset)"""
_select_end: Reactive[tuple[Widget, Offset, Offset] | None] = Reactive(None)

BINDINGS = [
Expand Down Expand Up @@ -1494,6 +1495,7 @@ def _watch__select_end(
Args:
select_end: The end selection.
"""

if select_end is None or self._select_start is None:
# Nothing to select
return
Expand All @@ -1509,7 +1511,8 @@ def _watch__select_end(
return

select_start, select_end = sorted(
[select_start, select_end], key=lambda selection: (selection[1].transpose)
[select_start, select_end],
key=lambda selection: (selection[0].region.offset.transpose),
)

start_widget, screen_start, start_offset = select_start
Expand Down
33 changes: 17 additions & 16 deletions src/textual/visual.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,11 @@ class Style:
strike: bool | None = None
link: str | None = None
_meta: bytes | None = None
x: int | None = None
y: int | None = None
auto_color: bool = False

def __rich_repr__(self) -> rich.repr.Result:
yield None, self.background
yield None, self.foreground
yield None, self.background, TRANSPARENT
yield None, self.foreground, TRANSPARENT
yield "bold", self.bold, None
yield "dim", self.dim, None
yield "italic", self.italic, None
Expand All @@ -135,14 +133,6 @@ def __rich_repr__(self) -> rich.repr.Result:
if self._meta is not None:
yield "meta", self.meta

yield "offset", self.offset, None

@property
def offset(self) -> tuple[int, int] | None:
if self.y is None:
return None
return (self.x or 0, self.y)

@lru_cache(maxsize=1024)
def __add__(self, other: object) -> Style:
if not isinstance(other, Style):
Expand All @@ -158,8 +148,6 @@ def __add__(self, other: object) -> Style:
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,
self.x if other.x is None else other.x,
self.y if other.y is None else other.y,
)
return new_style

Expand Down Expand Up @@ -236,8 +224,21 @@ def rich_style(self) -> RichStyle:
meta=self.meta,
)

def rich_style_with_offset(self, x: int, y: int) -> RichStyle:
return RichStyle(
color=(self.background + self.foreground).rich_color,
bgcolor=self.background.rich_color,
bold=self.bold,
dim=self.dim,
italic=self.italic,
underline=self.underline,
reverse=self.reverse,
strike=self.strike,
link=self.link,
meta={**self.meta, "offset": (x, y)},
)

def get_rich_style(self) -> RichStyle:
offset = self.offset
rich_style = RichStyle(
color=(self.background + self.foreground).rich_color,
bgcolor=self.background.rich_color,
Expand All @@ -248,7 +249,7 @@ def get_rich_style(self) -> RichStyle:
reverse=self.reverse,
strike=self.strike,
link=self.link,
meta=(self.meta if offset is None else {**self.meta, "offset": offset}),
meta=self.meta,
)
return rich_style

Expand Down

0 comments on commit c242510

Please sign in to comment.