diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 32192471ff..2346cb5e1c 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -75,7 +75,7 @@ def render_segments(self, console: Console) -> str: class LayoutUpdate(CompositorUpdate): """A renderable containing the result of a render for a given region.""" - def __init__(self, strips: list[Strip], region: Region) -> None: + def __init__(self, strips: list[Iterable[Strip]], region: Region) -> None: self.strips = strips self.region = region @@ -87,7 +87,8 @@ def __rich_console__( move_to = Control.move_to for last, (y, line) in loop_last(enumerate(self.strips, self.region.y)): yield move_to(x, y).segment - yield from line + for strip in line: + yield from strip if not last: yield new_line @@ -102,11 +103,12 @@ def render_segments(self, console: Console) -> str: """ sequences: list[str] = [] append = sequences.append + extend = sequences.extend x = self.region.x move_to = Control.move_to for last, (y, line) in loop_last(enumerate(self.strips, self.region.y)): append(move_to(x, y).segment.text) - append(line.render(console)) + extend([strip.render(console) for strip in line]) if not last: append("\n") return "".join(sequences) @@ -239,7 +241,6 @@ def render_segments(self, console: Console) -> str: Returns: Raw data with escape sequences. """ - sequences: list[str] = [] append = sequences.append @@ -613,8 +614,9 @@ def add_widget( - widget.scrollbar_size_horizontal ) ) - widget.set_reactive(Widget.scroll_y, new_scroll_y) - widget.set_reactive(Widget.scroll_target_y, new_scroll_y) + capped_scroll_y = widget.validate_scroll_y(new_scroll_y) + widget.set_reactive(Widget.scroll_y, capped_scroll_y) + widget.set_reactive(Widget.scroll_target_y, capped_scroll_y) widget.vertical_scrollbar._reactive_position = new_scroll_y if visible_only: @@ -1132,14 +1134,15 @@ def render_full_update(self, simplify: bool = False) -> LayoutUpdate: self._dirty_regions.clear() crop = screen_region chops = self._render_chops(crop, lambda y: True) + render_strips: list[Iterable[Strip]] if simplify: # Simplify is done when exporting to SVG # It doesn't make things faster render_strips = [ - Strip.join(chop.values()).simplify().discard_meta() for chop in chops + [Strip.join(chop.values()).simplify().discard_meta()] for chop in chops ] else: - render_strips = [Strip.join(chop.values()) for chop in chops] + render_strips = [chop.values() for chop in chops] return LayoutUpdate(render_strips, screen_region) @@ -1182,7 +1185,7 @@ def _render_chops( self, crop: Region, is_rendered_line: Callable[[int], bool], - ) -> Sequence[Mapping[int, Strip | None]]: + ) -> Sequence[Mapping[int, Strip]]: """Render update 'chops'. Args: @@ -1221,8 +1224,7 @@ def _render_chops( for cut, strip in zip(final_cuts, cut_strips): if get_chops_line(cut) is None: chops_line[cut] = strip - - return chops + return cast("Sequence[Mapping[int, Strip]]", chops) def __rich__(self) -> StripRenderable: return StripRenderable(self.render_strips()) diff --git a/src/textual/app.py b/src/textual/app.py index ad239dcea9..dd91424ffa 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -4576,9 +4576,11 @@ def suspend(self) -> Iterator[None]: # app, and we don't want to have the driver auto-restart # application mode when the application comes back to the # foreground, in this context. - with self._driver.no_automatic_restart(), redirect_stdout( - sys.__stdout__ - ), redirect_stderr(sys.__stderr__): + with ( + self._driver.no_automatic_restart(), + redirect_stdout(sys.__stdout__), + redirect_stderr(sys.__stderr__), + ): yield # We're done with the dev's code so resume application mode. self._driver.resume_application_mode() diff --git a/src/textual/content.py b/src/textual/content.py index b860bca845..8e2c3c716a 100644 --- a/src/textual/content.py +++ b/src/textual/content.py @@ -395,8 +395,8 @@ def assemble( def simplify(self) -> Content: """Simplify spans by joining contiguous spans together. - This can produce faster renders but typically only worth it if you have appended a - large number of Content instances together. + This may produce faster renders if you have concatenated a large number of small pieces + of content with repeating styles. Note that this modifies the Content instance in-place, which might appear to violate the immutability constraints, but it will not change the rendered output, diff --git a/src/textual/strip.py b/src/textual/strip.py index d516cea8d8..d97b5820ec 100644 --- a/src/textual/strip.py +++ b/src/textual/strip.py @@ -290,14 +290,21 @@ def join(cls, strips: Iterable[Strip | None]) -> Strip: Returns: A new combined strip. """ - join_strips = [strip for strip in strips if strip is not None] + join_strips = [ + strip for strip in strips if strip is not None and strip.cell_count + ] segments = [segment for strip in join_strips for segment in strip._segments] cell_length: int | None = None if any([strip._cell_length is None for strip in join_strips]): cell_length = None else: cell_length = sum([strip._cell_length or 0 for strip in join_strips]) - return cls(segments, cell_length) + joined_strip = cls(segments, cell_length) + if all(strip._render_cache is not None for strip in join_strips): + joined_strip._render_cache = "".join( + [strip._render_cache for strip in join_strips] + ) + return joined_strip def __add__(self, other: Strip) -> Strip: return Strip.join([self, other]) @@ -579,9 +586,7 @@ def divide(self, cuts: Iterable[int]) -> Sequence[Strip]: cell_length = self.cell_length cuts = [cut for cut in cuts if cut <= cell_length] cache_key = tuple(cuts) - cached = self._divide_cache.get(cache_key) - - if cached is not None: + if (cached := self._divide_cache.get(cache_key)) is not None: return cached strips: list[Strip] @@ -721,6 +726,7 @@ def render(self, console: Console) -> str: for text, style, _ in self._segments ] ) + return self._render_cache def crop_pad(self, cell_length: int, left: int, right: int, style: Style) -> Strip: @@ -805,5 +811,6 @@ def apply_offsets(self, x: int, y: int) -> Strip: ) x += len(segment.text) strip = Strip(strip_segments, self._cell_length) + strip._render_cache = self._render_cache self._offsets_cache[cache_key] = strip return strip diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 34946c399f..67da60ee61 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1485,7 +1485,10 @@ def _render_line(self, y: int) -> Strip: line_style = theme.base_style if theme else None text_strip = text_strip.extend_cell_length(target_width, line_style) - strip = Strip.join([Strip(gutter, cell_length=gutter_width), text_strip]) + if gutter: + strip = Strip.join([Strip(gutter, cell_length=gutter_width), text_strip]) + else: + strip = text_strip return strip.apply_style(base_style) @@ -2343,6 +2346,8 @@ def insert( Returns: An `EditResult` containing information about the edit. """ + if len(text) > 1: + self._restart_blink() if location is None: location = self.cursor_location return self.edit(Edit(text, location, location, maintain_selection_offset))