5
5
6
6
from rich .cells import cell_len
7
7
from rich .highlighter import Highlighter , ReprHighlighter
8
- from rich .segment import Segment
9
8
from rich .style import Style
10
9
from rich .text import Text
11
10
15
14
from textual .geometry import Size
16
15
from textual .reactive import var
17
16
from textual .scroll_view import ScrollView
17
+ from textual .selection import Selection
18
18
from textual .strip import Strip
19
19
20
20
if TYPE_CHECKING :
26
26
class Log (ScrollView , can_focus = True ):
27
27
"""A widget to log text."""
28
28
29
+ ALLOW_SELECT = True
29
30
DEFAULT_CSS = """
30
31
Log {
31
32
background: $surface;
@@ -75,6 +76,11 @@ def __init__(
75
76
self ._render_line_cache : LRUCache [int , Strip ] = LRUCache (1024 )
76
77
self .highlighter : Highlighter = ReprHighlighter ()
77
78
"""The Rich Highlighter object to use, if `highlight=True`"""
79
+ self ._clear_y = 0
80
+
81
+ @property
82
+ def allow_select (self ) -> bool :
83
+ return True
78
84
79
85
@property
80
86
def lines (self ) -> Sequence [str ]:
@@ -251,8 +257,25 @@ def clear(self) -> Self:
251
257
self ._render_line_cache .clear ()
252
258
self ._updates += 1
253
259
self .virtual_size = Size (0 , 0 )
260
+ self ._clear_y = 0
254
261
return self
255
262
263
+ def get_selection (self , selection : Selection ) -> tuple [str , str ] | None :
264
+ """Get the text under the selection.
265
+
266
+ Args:
267
+ selection: Selection information.
268
+
269
+ Returns:
270
+ Tuple of extracted text and ending (typically "\n " or " "), or `None` if no text could be extracted.
271
+ """
272
+ text = "\n " .join (self ._lines )
273
+ return selection .extract (text ), "\n "
274
+
275
+ def selection_updated (self , selection : Selection | None ) -> None :
276
+ self ._render_line_cache .clear ()
277
+ self .refresh ()
278
+
256
279
def render_line (self , y : int ) -> Strip :
257
280
"""Render a line of content.
258
281
@@ -284,6 +307,7 @@ def _render_line(self, y: int, scroll_x: int, width: int) -> Strip:
284
307
line = self ._render_line_strip (y , rich_style )
285
308
assert line ._cell_length is not None
286
309
line = line .crop_extend (scroll_x , scroll_x + width , rich_style )
310
+ line = line .apply_offsets (scroll_x , y )
287
311
return line
288
312
289
313
def _render_line_strip (self , y : int , rich_style : Style ) -> Strip :
@@ -296,18 +320,32 @@ def _render_line_strip(self, y: int, rich_style: Style) -> Strip:
296
320
Returns:
297
321
An uncropped Strip.
298
322
"""
299
- if y in self ._render_line_cache :
323
+ selection = self .selection
324
+ if y in self ._render_line_cache and self .selection is None :
300
325
return self ._render_line_cache [y ]
301
326
302
327
_line = self ._process_line (self ._lines [y ])
303
328
329
+ line_text = Text (_line , no_wrap = True )
330
+ line_text .stylize (rich_style )
331
+
304
332
if self .highlight :
305
- line_text = self .highlighter (Text (_line , style = rich_style , no_wrap = True ))
306
- line = Strip (line_text .render (self .app .console ), cell_len (_line ))
307
- else :
308
- line = Strip ([Segment (_line , rich_style )], cell_len (_line ))
333
+ line_text = self .highlighter (line_text )
334
+ if selection is not None :
335
+ if (select_span := selection .get_span (y - self ._clear_y )) is not None :
336
+ start , end = select_span
337
+ if end == - 1 :
338
+ end = len (line_text )
339
+
340
+ selection_style = self .screen .get_component_rich_style (
341
+ "screen--selection"
342
+ )
343
+ line_text .stylize (selection_style , start , end )
344
+
345
+ line = Strip (line_text .render (self .app .console ), cell_len (_line ))
309
346
310
- self ._render_line_cache [y ] = line
347
+ if selection is not None :
348
+ self ._render_line_cache [y ] = line
311
349
return line
312
350
313
351
def refresh_lines (self , y_start : int , line_count : int = 1 ) -> None :
0 commit comments