Skip to content

Commit df2b3a3

Browse files
authored
Merge pull request #5409 from Textualize/arbitrary-select
WIP Arbitrary select
2 parents 6bc9501 + 8607933 commit df2b3a3

File tree

386 files changed

+24341
-23099
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

386 files changed

+24341
-23099
lines changed

CHANGELOG.md

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,34 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## Unreleased
99

10+
### Changed
1011

11-
### Fixed
12-
13-
- Fixed `Pilot.click` not working with `times` parameter https://github.com/Textualize/textual/pull/5398
14-
- Fixed select refocusing itself too late https://github.com/Textualize/textual/pull/5420
12+
- Footer can now be scrolled horizontally without holding `shift` https://github.com/Textualize/textual/pull/5404
13+
- The content of an `Input` will now only be automatically selected when the widget is focused by the user, not when the app itself has regained focus (similar to web browsers). https://github.com/Textualize/textual/pull/5379
14+
- `Pilot.mouse_down` and `Pilot.mouse_up` now issue a prior `MouseMove` event, to more closely reflect real mouse actions. https://github.com/Textualize/textual/pull/5409
15+
- Snapshots tests now discard meta, which should reduce test breaking with no visual differences https://github.com/Textualize/textual/pull/5409
1516

1617
### Added
1718

19+
- Added `Select.type_to_search` which allows you to type to move the cursor to a matching option https://github.com/Textualize/textual/pull/5403
20+
- Updated `TextArea` and `Input` behavior when there is a selection and the user presses left or right https://github.com/Textualize/textual/pull/5400
1821
- Added `from_app_focus` to `Focus` event to indicate if a widget is being focused because the app itself has regained focus or not https://github.com/Textualize/textual/pull/5379
19-
- - Added `Select.type_to_search` which allows you to type to move the cursor to a matching option https://github.com/Textualize/textual/pull/5403
22+
- Added `Offset.transpose` https://github.com/Textualize/textual/pull/5409
23+
- Added `screen--selection` component class to define style for selection https://github.com/Textualize/textual/pull/5409
24+
- Added `Widget.select_container` property https://github.com/Textualize/textual/pull/5409
25+
- Added `Widget.select_all` https://github.com/Textualize/textual/pull/5409
26+
- Added `Region.bottom_right_inclusive` https://github.com/Textualize/textual/pull/5409
27+
- Added double click to select, triple click to select all in container https://github.com/Textualize/textual/pull/5409
28+
- Added arbitrary text selection https://github.com/Textualize/textual/pull/5409
29+
- Added Widget.ALLOW_SELECT classvar for a per-widget switch to disable text selection https://github.com/Textualize/textual/pull/5409
30+
- Added Widget.allow_select method for programmatic control of text selection https://github.com/Textualize/textual/pull/5409
31+
- Added App.ALLOW_SELECT for a global switch to disable text selection https://github.com/Textualize/textual/pull/5409
32+
- Added `DOMNode.query_ancestor` https://github.com/Textualize/textual/pull/5409
2033

21-
### Changed
34+
### Fixed
2235

23-
- The content of an `Input` will now only be automatically selected when the widget is focused by the user, not when the app itself has regained focus (similar to web browsers). https://github.com/Textualize/textual/pull/5379
24-
- Updated `TextArea` and `Input` behavior when there is a selection and the user presses left or right https://github.com/Textualize/textual/pull/5400
25-
- Footer can now be scrolled horizontally without holding `shift` https://github.com/Textualize/textual/pull/5404
36+
- Fixed `Pilot.click` not working with `times` parameter https://github.com/Textualize/textual/pull/5398
37+
- Fixed select refocusing itself too late https://github.com/Textualize/textual/pull/5420
2638

2739

2840
## [1.0.0] - 2024-12-12

poetry.lock

Lines changed: 203 additions & 209 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/textual/_ansi_theme.py

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,71 @@
1+
from __future__ import annotations
2+
13
from rich.terminal_theme import TerminalTheme
24

5+
6+
def rgb(red: int, green: int, blue: int) -> tuple[int, int, int]:
7+
"""Define an RGB color.
8+
9+
This exists mainly so that a VSCode extension can render the colors inline.
10+
11+
Args:
12+
red: Red component.
13+
green: Green component.
14+
blue: Blue component.
15+
16+
Returns:
17+
Color triplet.
18+
"""
19+
return red, green, blue
20+
21+
322
MONOKAI = TerminalTheme(
4-
(12, 12, 12),
5-
(217, 217, 217),
23+
rgb(12, 12, 12),
24+
rgb(217, 217, 217),
625
[
7-
(26, 26, 26),
8-
(244, 0, 95),
9-
(152, 224, 36),
10-
(253, 151, 31),
11-
(157, 101, 255),
12-
(244, 0, 95),
13-
(88, 209, 235),
14-
(196, 197, 181),
15-
(98, 94, 76),
26+
rgb(26, 26, 26),
27+
rgb(244, 0, 95),
28+
rgb(152, 224, 36),
29+
rgb(253, 151, 31),
30+
rgb(157, 101, 255),
31+
rgb(244, 0, 95),
32+
rgb(88, 209, 235),
33+
rgb(196, 197, 181),
34+
rgb(98, 94, 76),
1635
],
1736
[
18-
(244, 0, 95),
19-
(152, 224, 36),
20-
(224, 213, 97),
21-
(157, 101, 255),
22-
(244, 0, 95),
23-
(88, 209, 235),
24-
(246, 246, 239),
37+
rgb(244, 0, 95),
38+
rgb(152, 224, 36),
39+
rgb(224, 213, 97),
40+
rgb(157, 101, 255),
41+
rgb(244, 0, 95),
42+
rgb(88, 209, 235),
43+
rgb(246, 246, 239),
2544
],
2645
)
2746

2847
ALABASTER = TerminalTheme(
29-
(247, 247, 247),
30-
(0, 0, 0),
48+
rgb(247, 247, 247),
49+
rgb(0, 0, 0),
3150
[
32-
(0, 0, 0),
33-
(170, 55, 49),
34-
(68, 140, 39),
35-
(203, 144, 0),
36-
(50, 92, 192),
37-
(122, 62, 157),
38-
(0, 131, 178),
39-
(247, 247, 247),
40-
(119, 119, 119),
51+
rgb(0, 0, 0),
52+
rgb(170, 55, 49),
53+
rgb(68, 140, 39),
54+
rgb(203, 144, 0),
55+
rgb(50, 92, 192),
56+
rgb(122, 62, 157),
57+
rgb(0, 131, 178),
58+
rgb(247, 247, 247),
59+
rgb(119, 119, 119),
4160
],
4261
[
43-
(240, 80, 80),
44-
(96, 203, 0),
45-
(255, 188, 93),
46-
(0, 122, 204),
47-
(230, 76, 230),
48-
(0, 170, 203),
49-
(247, 247, 247),
62+
rgb(240, 80, 80),
63+
rgb(96, 203, 0),
64+
rgb(255, 188, 93),
65+
rgb(0, 122, 204),
66+
rgb(230, 76, 230),
67+
rgb(0, 170, 203),
68+
rgb(247, 247, 247),
5069
],
5170
)
5271

src/textual/_compositor.py

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -844,11 +844,11 @@ def get_style_at(self, x: int, y: int) -> Style:
844844
"""Get the Style at the given cell or Style.null()
845845
846846
Args:
847-
x: X position within the Layout
848-
y: Y position within the Layout
847+
x: X position within the Layout.
848+
y: Y position within the Layout.
849849
850850
Returns:
851-
The Style at the cell (x, y) within the Layout
851+
The Style at the cell (x, y) within the Layout.
852852
"""
853853
try:
854854
widget, region = self.get_widget_at(x, y)
@@ -866,12 +866,70 @@ def get_style_at(self, x: int, y: int) -> Style:
866866
if not lines:
867867
return Style.null()
868868
end = 0
869+
869870
for segment in lines[0]:
870871
end += segment.cell_length
871872
if x < end:
872873
return segment.style or Style.null()
874+
873875
return Style.null()
874876

877+
def get_widget_and_offset_at(
878+
self, x: int, y: int
879+
) -> tuple[Widget | None, Offset | None]:
880+
"""Get the Style at the given cell, the offset within the content.
881+
882+
Args:
883+
x: X position within the Layout.
884+
y: Y position within the Layout.
885+
886+
Returns:
887+
A tuple of the widget at (x, y) and the offset within the widget.
888+
"""
889+
try:
890+
widget, region = self.get_widget_at(x, y)
891+
except errors.NoWidget:
892+
return None, None
893+
if widget not in self.visible_widgets:
894+
return None, None
895+
896+
if y >= widget.content_region.bottom:
897+
x, y = widget.content_region.bottom_right_inclusive
898+
899+
x -= region.x
900+
y -= region.y
901+
902+
visible_screen_stack.set(widget.app._background_screens)
903+
lines = widget.render_lines(Region(0, y, region.width, 1))
904+
905+
if not lines:
906+
return widget, None
907+
end = 0
908+
start = 0
909+
910+
offset_y: int | None = None
911+
offset_x = 0
912+
offset_x2 = 0
913+
914+
for segment in lines[0]:
915+
end += segment.cell_length
916+
style = segment.style
917+
if style is not None and style._meta is not None:
918+
meta = style.meta
919+
if "offset" in meta:
920+
offset_x, offset_y = style.meta["offset"]
921+
offset_x2 = offset_x + segment.cell_length
922+
923+
if x <= end:
924+
return widget, (
925+
None
926+
if offset_y is None
927+
else Offset(offset_x + (x - start), offset_y)
928+
)
929+
start = end
930+
931+
return widget, (None if offset_y is None else Offset(offset_x2, offset_y))
932+
875933
def find_widget(self, widget: Widget) -> MapGeometry:
876934
"""Get information regarding the relative position of a widget in the Compositor.
877935
@@ -1056,7 +1114,9 @@ def render_full_update(self, simplify: bool = False) -> LayoutUpdate:
10561114
crop = screen_region
10571115
chops = self._render_chops(crop, lambda y: True)
10581116
if simplify:
1059-
render_strips = [Strip.join(chop.values()).simplify() for chop in chops]
1117+
render_strips = [
1118+
Strip.join(chop.values()).simplify().discard_meta() for chop in chops
1119+
]
10601120
else:
10611121
render_strips = [Strip.join(chop.values()) for chop in chops]
10621122

src/textual/_styles_cache.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from rich.console import Console
88
from rich.segment import Segment
99
from rich.style import Style
10+
from rich.terminal_theme import TerminalTheme
1011
from rich.text import Text
1112

1213
from textual import log
@@ -144,6 +145,7 @@ def render_widget(self, widget: Widget, crop: Region) -> list[Strip]:
144145
crop=crop,
145146
filters=widget.app._filters,
146147
opacity=widget.opacity,
148+
ansi_theme=widget.app.ansi_theme,
147149
)
148150
if widget.auto_links:
149151
hover_style = widget.hover_style
@@ -176,6 +178,7 @@ def render(
176178
crop: Region | None = None,
177179
filters: Sequence[LineFilter] | None = None,
178180
opacity: float = 1.0,
181+
ansi_theme: TerminalTheme = DEFAULT_TERMINAL_THEME,
179182
) -> list[Strip]:
180183
"""Render a widget content plus CSS styles.
181184
@@ -231,6 +234,7 @@ def render(
231234
border_title,
232235
border_subtitle,
233236
opacity,
237+
ansi_theme,
234238
)
235239
self._cache[y] = strip
236240
else:
@@ -267,6 +271,7 @@ def render_line(
267271
border_title: tuple[Text, Color, Color, Style] | None,
268272
border_subtitle: tuple[Text, Color, Color, Style] | None,
269273
opacity: float,
274+
ansi_theme: TerminalTheme,
270275
) -> Strip:
271276
"""Render a styled line.
272277
@@ -447,7 +452,9 @@ def post(segments: Iterable[Segment]) -> Iterable[Segment]:
447452
if inner:
448453
line = Segment.apply_style(line, inner)
449454
if styles.text_opacity != 1.0:
450-
line = TextOpacity.process_segments(line, styles.text_opacity)
455+
line = TextOpacity.process_segments(
456+
line, styles.text_opacity, ansi_theme
457+
)
451458
line = line_post(line_pad(line, pad_left, pad_right, inner))
452459

453460
if border_left or border_right:

src/textual/app.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,12 @@ class MyApp(App[None]):
396396
Setting to `None` or `""` disables auto focus.
397397
"""
398398

399+
ALLOW_SELECT: ClassVar[bool] = True
400+
"""A switch to toggle arbitrary text selection for the app.
401+
402+
Note that this doesn't apply to Input and TextArea which have builtin support for selection.
403+
"""
404+
399405
_BASE_PATH: str | None = None
400406
CSS_PATH: ClassVar[CSSPathType | None] = None
401407
"""File paths to load CSS from."""
@@ -1531,7 +1537,6 @@ def copy_to_clipboard(self, text: str) -> None:
15311537
self._clipboard = text
15321538
if self._driver is None:
15331539
return
1534-
15351540
import base64
15361541

15371542
base64_text = base64.b64encode(text.encode("utf-8")).decode("utf-8")

src/textual/color.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ def automatic(cls, alpha_percentage: float = 100.0) -> Color:
177177
return cls(0, 0, 0, alpha_percentage / 100.0, auto=True)
178178

179179
@classmethod
180+
@lru_cache(maxsize=1024)
180181
def from_rich_color(
181182
cls, rich_color: RichColor | None, theme: TerminalTheme | None = None
182183
) -> Color:
@@ -192,7 +193,9 @@ def from_rich_color(
192193
if rich_color is None:
193194
return TRANSPARENT
194195
r, g, b = rich_color.get_truecolor(theme)
195-
return cls(r, g, b)
196+
return cls(
197+
r, g, b, ansi=rich_color.number if rich_color.is_system_defined else None
198+
)
196199

197200
@classmethod
198201
def from_hsl(cls, h: float, s: float, l: float) -> Color:

0 commit comments

Comments
 (0)