Skip to content

Commit 617f1bd

Browse files
committed
select mechanics
1 parent c46b906 commit 617f1bd

File tree

12 files changed

+162
-69
lines changed

12 files changed

+162
-69
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1717
- 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
1818
- Updated `TextArea` and `Input` behavior when there is a selection and the user presses left or right https://github.com/Textualize/textual/pull/5400
1919
- 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
20+
- Added `Offset.transpose`
21+
- Added `screen--selection` component class to define style for selection
22+
- Added `Widget.scrollable_container` property
23+
- Added `Widget.select_all`
2024

2125
### Fixed
2226

src/textual/_ansi_theme.py

+53-36
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,69 @@
11
from rich.terminal_theme import TerminalTheme
22

3+
4+
def rgb(red: int, green: int, blue: int) -> tuple[int, int, int]:
5+
"""Define an RGB color.
6+
7+
This exists mainly so that a VSCode extension can render the colors inline.
8+
9+
Args:
10+
red: Red component.
11+
green: Green component.
12+
blue: Blue component.
13+
14+
Returns:
15+
Color triplet.
16+
"""
17+
return red, green, blue
18+
19+
320
MONOKAI = TerminalTheme(
4-
(12, 12, 12),
5-
(217, 217, 217),
21+
rgb(12, 12, 12),
22+
rgb(217, 217, 217),
623
[
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),
24+
rgb(26, 26, 26),
25+
rgb(244, 0, 95),
26+
rgb(152, 224, 36),
27+
rgb(253, 151, 31),
28+
rgb(157, 101, 255),
29+
rgb(244, 0, 95),
30+
rgb(88, 209, 235),
31+
rgb(196, 197, 181),
32+
rgb(98, 94, 76),
1633
],
1734
[
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),
35+
rgb(244, 0, 95),
36+
rgb(152, 224, 36),
37+
rgb(224, 213, 97),
38+
rgb(157, 101, 255),
39+
rgb(244, 0, 95),
40+
rgb(88, 209, 235),
41+
rgb(246, 246, 239),
2542
],
2643
)
2744

2845
ALABASTER = TerminalTheme(
29-
(247, 247, 247),
30-
(0, 0, 0),
46+
rgb(247, 247, 247),
47+
rgb(0, 0, 0),
3148
[
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),
49+
rgb(0, 0, 0),
50+
rgb(170, 55, 49),
51+
rgb(68, 140, 39),
52+
rgb(203, 144, 0),
53+
rgb(50, 92, 192),
54+
rgb(122, 62, 157),
55+
rgb(0, 131, 178),
56+
rgb(247, 247, 247),
57+
rgb(119, 119, 119),
4158
],
4259
[
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),
60+
rgb(240, 80, 80),
61+
rgb(96, 203, 0),
62+
rgb(255, 188, 93),
63+
rgb(0, 122, 204),
64+
rgb(230, 76, 230),
65+
rgb(0, 170, 203),
66+
rgb(247, 247, 247),
5067
],
5168
)
5269

src/textual/_compositor.py

+43-16
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,7 @@ def get_widget_and_offset_at(
884884
y: Y position within the Layout.
885885
886886
Returns:
887-
A tuple of the Style at the cell (x, y) and the offset within the widget.
887+
A tuple of the widget at (x, y) and the offset within the widget.
888888
"""
889889
try:
890890
widget, region = self.get_widget_at(x, y)
@@ -893,9 +893,10 @@ def get_widget_and_offset_at(
893893
if widget not in self.visible_widgets:
894894
return None, None
895895

896-
if y >= widget.content_region.bottom:
897-
# If y is below the content region, default to the offset on the next line
898-
return widget, Offset(x - region.x, widget.content_region.bottom)
896+
# if y >= widget.content_region.bottom:
897+
# # If y is below the content region, default to the offset on the next line
898+
# # y = widget.content_region.bottom
899+
# return widget, Offset(x - region.x, widget.content_region.bottom)
899900

900901
x -= region.x
901902
y -= region.y
@@ -907,19 +908,43 @@ def get_widget_and_offset_at(
907908
return widget, None
908909
end = 0
909910
start = 0
911+
912+
offset_y: int | None = None
913+
offset_x = 0
914+
offset_x2 = 0
915+
910916
for segment in lines[0]:
911917
end += segment.cell_length
912-
if x <= end:
913-
style = segment.style
914-
if style and style._meta is not None:
915-
meta = style.meta
916-
if "offset" in meta:
917-
offset_x, offset_y = style.meta["offset"]
918-
offset = Offset(offset_x + (x - start), offset_y)
919-
return widget, offset
920-
921-
return widget, None
918+
style = segment.style
919+
if style is not None and style._meta is not None:
920+
meta = style.meta
921+
if "offset" in meta:
922+
offset_x, offset_y = style.meta["offset"]
923+
offset_x2 = offset_x + segment.cell_length
924+
925+
if x <= end:
926+
return widget, (
927+
None
928+
if offset_y is None
929+
else Offset(offset_x + (x - start), offset_y)
930+
)
922931
start = end
932+
933+
return widget, (None if offset_y is None else Offset(offset_x2, offset_y))
934+
935+
# for segment in lines[0]:
936+
# end += segment.cell_length
937+
# if x <= end:
938+
# style = segment.style
939+
# if style and style._meta is not None:
940+
# meta = style.meta
941+
# if "offset" in meta:
942+
# offset_x, offset_y = style.meta["offset"]
943+
# offset = Offset(offset_x + (x - start), offset_y)
944+
# return widget, offset
945+
946+
# return widget, None
947+
# start = end
923948
return widget, None
924949

925950
def find_widget(self, widget: Widget) -> MapGeometry:
@@ -1106,9 +1131,11 @@ def render_full_update(self, simplify: bool = False) -> LayoutUpdate:
11061131
crop = screen_region
11071132
chops = self._render_chops(crop, lambda y: True)
11081133
if simplify:
1109-
render_strips = [Strip.join(chop.values()).simplify() for chop in chops]
1134+
render_strips = [
1135+
Strip.join(chop.values()).discard_meta().simplify() for chop in chops
1136+
]
11101137
else:
1111-
render_strips = [Strip.join(chop.values()) for chop in chops]
1138+
render_strips = [Strip.join(chop.values()).discard_meta() for chop in chops]
11121139

11131140
return LayoutUpdate(render_strips, screen_region)
11141141

src/textual/color.py

+5-2
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:
@@ -400,7 +403,7 @@ def blend(
400403
"""
401404
if destination.auto:
402405
destination = self.get_contrast_text(destination.a)
403-
if destination.ansi is not None:
406+
if destination.ansi is not None or self.ansi:
404407
return destination
405408
if factor <= 0:
406409
return self

src/textual/content.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from __future__ import annotations
1212

1313
import re
14+
from functools import lru_cache
1415
from operator import itemgetter
1516
from typing import TYPE_CHECKING, Callable, Iterable, NamedTuple, Sequence
1617

@@ -132,6 +133,7 @@ def from_rich_text(
132133
"""
133134
if isinstance(text, str):
134135
text = Text.from_markup(text)
136+
135137
if text._spans:
136138
ansi_theme: TerminalTheme | None
137139
try:
@@ -153,13 +155,20 @@ def from_rich_text(
153155
else:
154156
spans = []
155157

156-
return cls(
158+
content = cls(
157159
text.plain,
158160
spans,
159161
align=align,
160162
no_wrap=no_wrap,
161163
ellipsis=ellipsis,
162164
)
165+
if text.style:
166+
content = content.stylize_before(
167+
text.style
168+
if isinstance(text.style, str)
169+
else Style.from_rich_style(text.style, ansi_theme)
170+
)
171+
return content
163172

164173
@classmethod
165174
def styled(
@@ -284,10 +293,6 @@ def render_strips(
284293
if height is not None:
285294
lines = lines[:height]
286295

287-
# strip_lines = [
288-
# Strip(line.content.render_segments(style), line.content.cell_length)
289-
# for line in lines
290-
# ]
291296
strip_lines = [line.to_strip(style) for line in lines]
292297
return strip_lines
293298

@@ -716,12 +721,12 @@ def render(
716721
app = active_app.get()
717722
# TODO: Update when we add Content.from_markup
718723

724+
@lru_cache(maxsize=1024)
719725
def get_style(style: str, /) -> Style:
720-
return (
721-
Style.from_rich_style(app.console.get_style(style), app.ansi_theme)
722-
if isinstance(style, str)
723-
else style
726+
visual_style = Style.from_rich_style(
727+
app.console.get_style(style), app.ansi_theme
724728
)
729+
return visual_style
725730

726731
else:
727732
get_style = parse_style

src/textual/screen.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -1456,14 +1456,15 @@ def _forward_event(self, event: events.Event) -> None:
14561456
select_widget, select_offset = self.get_widget_and_offset_at(
14571457
event.screen_x, event.screen_y
14581458
)
1459-
if select_widget is not None and select_widget.ALLOW_SELECT:
1459+
if select_widget is not None and select_widget.allow_select:
14601460
self._selecting = True
14611461
if select_widget is not None and select_offset is not None:
14621462
self._select_start = (
14631463
select_widget,
14641464
event.screen_offset,
14651465
select_offset,
14661466
)
1467+
14671468
else:
14681469
self._selection = False
14691470

@@ -1484,7 +1485,11 @@ def _forward_event(self, event: events.Event) -> None:
14841485
select_widget, select_offset = self.get_widget_and_offset_at(
14851486
event.x, event.y
14861487
)
1487-
if select_widget is not None and select_offset is not None:
1488+
if (
1489+
select_widget is not None
1490+
and select_widget.allow_select
1491+
and select_offset is not None
1492+
):
14881493
self._select_end = (select_widget, event.offset, select_offset)
14891494

14901495
if isinstance(event, events.MouseEvent):

src/textual/strip.py

+18
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,24 @@ def simplify(self) -> Strip:
394394
)
395395
return line
396396

397+
def discard_meta(self) -> Strip:
398+
"""Remove all meta from segments.
399+
400+
Returns:
401+
New strip.
402+
"""
403+
404+
def remove_meta_from_segment(segment: Segment) -> Segment:
405+
if segment.style is None:
406+
return segment
407+
text, style, control = segment
408+
style = style.copy()
409+
style._meta = None
410+
411+
return Segment(text, style, control)
412+
413+
return Strip([segment for segment in self._segments], self._cell_length)
414+
397415
def apply_filter(self, filter: LineFilter, background: Color) -> Strip:
398416
"""Apply a filter to all segments in the strip.
399417

src/textual/visual.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,10 @@ def visualize(widget: Widget, obj: object) -> Visual:
8686
if isinstance(obj, str):
8787
obj = widget.render_str(obj)
8888

89-
if isinstance(obj, Text) and widget.ALLOW_SELECT:
90-
return Content.from_rich_text(obj, align=widget.styles.text_align)
89+
if isinstance(obj, Text) and widget.allow_select:
90+
return Content.from_rich_text(
91+
obj, align=obj.align or widget.styles.text_align
92+
)
9193

9294
# If its is a Rich renderable, wrap it with a RichVisual
9395
return RichVisual(widget, rich_cast(obj))
@@ -173,6 +175,7 @@ def from_rich_style(
173175
underline=rich_style.underline,
174176
reverse=rich_style.reverse,
175177
strike=rich_style.strike,
178+
_meta=rich_style._meta,
176179
)
177180

178181
@classmethod

0 commit comments

Comments
 (0)