Skip to content

Commit c26e167

Browse files
committed
Add show cursor boolean
1 parent db623fa commit c26e167

File tree

1 file changed

+84
-18
lines changed

1 file changed

+84
-18
lines changed

src/textual/widgets/_text_area.py

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ class TextArea(ScrollView):
172172
&.-read-only .text-area--cursor {
173173
background: $warning-darken-1;
174174
}
175-
}
175+
}
176176
}
177177
"""
178178

@@ -373,6 +373,15 @@ class TextArea(ScrollView):
373373
The document can still be edited programmatically via the API.
374374
"""
375375

376+
show_cursor: Reactive[bool] = reactive(True)
377+
"""Show the cursor in read only mode?
378+
379+
If `False`, the cursor will be visible when `read_only==True`.
380+
If `True`, the cursor will not be visible when `read_only==True`, and the TextArea may be
381+
scroll like other containers.
382+
383+
"""
384+
376385
compact: reactive[bool] = reactive(False, toggle_class="-textual-compact")
377386
"""Enable compact display?"""
378387

@@ -423,6 +432,7 @@ def __init__(
423432
soft_wrap: bool = True,
424433
tab_behavior: Literal["focus", "indent"] = "focus",
425434
read_only: bool = False,
435+
show_cursor: bool = True,
426436
show_line_numbers: bool = False,
427437
line_number_start: int = 1,
428438
max_checkpoints: int = 50,
@@ -443,6 +453,7 @@ def __init__(
443453
soft_wrap: Enable soft wrapping.
444454
tab_behavior: If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.
445455
read_only: Enable read-only mode. This prevents edits using the keyboard.
456+
show_cursor: Show the cursor in read only mode (no effect otherwise).
446457
show_line_numbers: Show line numbers on the left edge.
447458
line_number_start: What line number to start on.
448459
max_checkpoints: The maximum number of undo history checkpoints to retain.
@@ -509,6 +520,7 @@ def __init__(
509520

510521
self.set_reactive(TextArea.soft_wrap, soft_wrap)
511522
self.set_reactive(TextArea.read_only, read_only)
523+
self.set_reactive(TextArea.show_cursor, show_cursor)
512524
self.set_reactive(TextArea.show_line_numbers, show_line_numbers)
513525
self.set_reactive(TextArea.line_number_start, line_number_start)
514526
self.set_reactive(TextArea.highlight_cursor_line, highlight_cursor_line)
@@ -542,6 +554,7 @@ def code_editor(
542554
soft_wrap: bool = False,
543555
tab_behavior: Literal["focus", "indent"] = "indent",
544556
read_only: bool = False,
557+
show_cursor: bool = True,
545558
show_line_numbers: bool = True,
546559
line_number_start: int = 1,
547560
max_checkpoints: int = 50,
@@ -564,6 +577,8 @@ def code_editor(
564577
theme: The theme to use.
565578
soft_wrap: Enable soft wrapping.
566579
tab_behavior: If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.
580+
read_only: Enable read-only mode. This prevents edits using the keyboard.
581+
show_cursor: Show the cursor in read only mode (no effect otherwise).
567582
show_line_numbers: Show line numbers on the left edge.
568583
line_number_start: What line number to start on.
569584
name: The name of the `TextArea` widget.
@@ -581,6 +596,7 @@ def code_editor(
581596
soft_wrap=soft_wrap,
582597
tab_behavior=tab_behavior,
583598
read_only=read_only,
599+
show_cursor=show_cursor,
584600
show_line_numbers=show_line_numbers,
585601
line_number_start=line_number_start,
586602
max_checkpoints=max_checkpoints,
@@ -1101,6 +1117,24 @@ def _refresh_size(self) -> None:
11011117
width, height = self.document.get_size(self.indent_width)
11021118
self.virtual_size = Size(width + self.gutter_width + 1, height)
11031119

1120+
@property
1121+
def _draw_cursor(self) -> bool:
1122+
"""Draw the cursor?"""
1123+
if self.read_only:
1124+
# If we are in read only mode, we don't want the cursor to blink
1125+
return self.show_cursor and self.has_focus
1126+
draw_cursor = (
1127+
self.has_focus
1128+
and not self.cursor_blink
1129+
or (self.cursor_blink and self._cursor_visible)
1130+
)
1131+
return draw_cursor
1132+
1133+
@property
1134+
def _has_cursor(self) -> bool:
1135+
"""Is there a usable cursor?"""
1136+
return not (self.read_only and not self.show_cursor)
1137+
11041138
def get_line(self, line_index: int) -> Text:
11051139
"""Retrieve the line at the given line index.
11061140
@@ -1158,6 +1192,7 @@ def render_line(self, y: int) -> Strip:
11581192
self.soft_wrap,
11591193
self.show_line_numbers,
11601194
self.read_only,
1195+
self.show_cursor,
11611196
)
11621197
if (cached_line := self._line_cache.get(cache_key)) is not None:
11631198
return cached_line
@@ -1213,12 +1248,13 @@ def _render_line(self, y: int) -> Strip:
12131248
selection_top_row, selection_top_column = selection_top
12141249
selection_bottom_row, selection_bottom_column = selection_bottom
12151250

1216-
cursor_line_style = theme.cursor_line_style if theme else None
1217-
if (
1218-
cursor_line_style
1219-
and cursor_row == line_index
1220-
and self.highlight_cursor_line
1221-
):
1251+
highlight_cursor_line = self.highlight_cursor_line and self._has_cursor
1252+
cursor_line_style = (
1253+
theme.cursor_line_style if (theme and highlight_cursor_line) else None
1254+
)
1255+
has_cursor = self._has_cursor
1256+
1257+
if has_cursor and cursor_line_style and cursor_row == line_index:
12221258
line.stylize(cursor_line_style)
12231259

12241260
# Selection styling
@@ -1271,16 +1307,14 @@ def _render_line(self, y: int) -> Strip:
12711307
matching_bracket = self._matching_bracket_location
12721308
match_cursor_bracket = self.match_cursor_bracket
12731309
draw_matched_brackets = (
1274-
match_cursor_bracket and matching_bracket is not None and start == end
1310+
has_cursor
1311+
and match_cursor_bracket
1312+
and matching_bracket is not None
1313+
and start == end
12751314
)
12761315

12771316
if cursor_row == line_index:
1278-
draw_cursor = (
1279-
self.has_focus
1280-
and not self.cursor_blink
1281-
or (self.cursor_blink and self._cursor_visible)
1282-
and not self.read_only
1283-
)
1317+
draw_cursor = self._draw_cursor
12841318
if draw_matched_brackets:
12851319
matching_bracket_style = theme.bracket_matching_style if theme else None
12861320
if matching_bracket_style:
@@ -1312,7 +1346,7 @@ def _render_line(self, y: int) -> Strip:
13121346
# Build the gutter text for this line
13131347
gutter_width = self.gutter_width
13141348
if self.show_line_numbers:
1315-
if cursor_row == line_index and self.highlight_cursor_line:
1349+
if cursor_row == line_index and highlight_cursor_line:
13161350
gutter_style = theme.cursor_line_gutter_style
13171351
else:
13181352
gutter_style = theme.gutter_style
@@ -1565,8 +1599,6 @@ def _redo_batch(self, edits: Sequence[Edit]) -> None:
15651599
async def _on_key(self, event: events.Key) -> None:
15661600
"""Handle key presses which correspond to document inserts."""
15671601
self._restart_blink()
1568-
if self.read_only:
1569-
return
15701602

15711603
key = event.key
15721604
insert_values = {
@@ -1584,6 +1616,9 @@ async def _on_key(self, event: events.Key) -> None:
15841616
insert_values["tab"] = " " * self._find_columns_to_next_tab_stop()
15851617

15861618
if event.is_printable or key in insert_values:
1619+
if self.read_only:
1620+
self.app.bell()
1621+
return
15871622
event.stop()
15881623
event.prevent_default()
15891624
insert = insert_values.get(key, event.character)
@@ -1775,6 +1810,8 @@ def scroll_cursor_visible(
17751810
Returns:
17761811
The offset that was scrolled to bring the cursor into view.
17771812
"""
1813+
if not self._has_cursor:
1814+
return Offset(0, 0)
17781815
self._recompute_cursor_offset()
17791816

17801817
x, y = self._cursor_offset
@@ -1804,6 +1841,8 @@ def move_cursor(
18041841
so that we jump back to the same width the next time we move to a row
18051842
that is wide enough.
18061843
"""
1844+
if not self._has_cursor:
1845+
return
18071846
if select:
18081847
start, _end = self.selection
18091848
self.selection = Selection(start, location)
@@ -1948,6 +1987,9 @@ def action_cursor_left(self, select: bool = False) -> None:
19481987
Args:
19491988
select: If True, select the text while moving.
19501989
"""
1990+
if not self._has_cursor:
1991+
self.scroll_left()
1992+
return
19511993
target = (
19521994
self.get_cursor_left_location()
19531995
if select or self.selection.is_empty
@@ -1973,6 +2015,9 @@ def action_cursor_right(self, select: bool = False) -> None:
19732015
Args:
19742016
select: If True, select the text while moving.
19752017
"""
2018+
if not self._has_cursor:
2019+
self.scroll_right()
2020+
return
19762021
target = (
19772022
self.get_cursor_right_location()
19782023
if select or self.selection.is_empty
@@ -1994,6 +2039,9 @@ def action_cursor_down(self, select: bool = False) -> None:
19942039
Args:
19952040
select: If True, select the text while moving.
19962041
"""
2042+
if not self._has_cursor:
2043+
self.scroll_down()
2044+
return
19972045
target = self.get_cursor_down_location()
19982046
self.move_cursor(target, record_width=False, select=select)
19992047

@@ -2011,6 +2059,9 @@ def action_cursor_up(self, select: bool = False) -> None:
20112059
Args:
20122060
select: If True, select the text while moving.
20132061
"""
2062+
if not self._has_cursor:
2063+
self.scroll_up()
2064+
return
20142065
target = self.get_cursor_up_location()
20152066
self.move_cursor(target, record_width=False, select=select)
20162067

@@ -2024,6 +2075,9 @@ def get_cursor_up_location(self) -> Location:
20242075

20252076
def action_cursor_line_end(self, select: bool = False) -> None:
20262077
"""Move the cursor to the end of the line."""
2078+
if not self._has_cursor:
2079+
self.scroll_x = self.max_scroll_x
2080+
return
20272081
location = self.get_cursor_line_end_location()
20282082
self.move_cursor(location, select=select)
20292083

@@ -2037,6 +2091,9 @@ def get_cursor_line_end_location(self) -> Location:
20372091

20382092
def action_cursor_line_start(self, select: bool = False) -> None:
20392093
"""Move the cursor to the start of the line."""
2094+
if not self._has_cursor:
2095+
self.scroll_x = 0
2096+
return
20402097
target = self.get_cursor_line_start_location(smart_home=True)
20412098
self.move_cursor(target, select=select)
20422099

@@ -2061,6 +2118,8 @@ def action_cursor_word_left(self, select: bool = False) -> None:
20612118
Args:
20622119
select: Whether to select while moving the cursor.
20632120
"""
2121+
if not self.show_cursor:
2122+
return
20642123
if self.cursor_at_start_of_text:
20652124
return
20662125
target = self.get_cursor_word_left_location()
@@ -2086,7 +2145,8 @@ def get_cursor_word_left_location(self) -> Location:
20862145

20872146
def action_cursor_word_right(self, select: bool = False) -> None:
20882147
"""Move the cursor right by a single word, skipping leading whitespace."""
2089-
2148+
if not self.show_cursor:
2149+
return
20902150
if self.cursor_at_end_of_text:
20912151
return
20922152

@@ -2121,6 +2181,9 @@ def get_cursor_word_right_location(self) -> Location:
21212181

21222182
def action_cursor_page_up(self) -> None:
21232183
"""Move the cursor and scroll up one page."""
2184+
if not self.show_cursor:
2185+
self.scroll_page_up()
2186+
return
21242187
height = self.content_size.height
21252188
_, cursor_location = self.selection
21262189
target = self.navigator.get_location_at_y_offset(
@@ -2132,6 +2195,9 @@ def action_cursor_page_up(self) -> None:
21322195

21332196
def action_cursor_page_down(self) -> None:
21342197
"""Move the cursor and scroll down one page."""
2198+
if not self.show_cursor:
2199+
self.scroll_page_down()
2200+
return
21352201
height = self.content_size.height
21362202
_, cursor_location = self.selection
21372203
target = self.navigator.get_location_at_y_offset(

0 commit comments

Comments
 (0)