Skip to content

Commit d10791d

Browse files
authored
Merge pull request #5510 from Textualize/new-option-list
WIP new option list
2 parents 5f7355c + 310883d commit d10791d

Some content is hidden

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

42 files changed

+2600
-2075
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
4444
- Fixed scrollbars ignoring background opacity https://github.com/Textualize/textual/issues/5458
4545
- Fixed `Header` icon showing command palette tooltip when disabled https://github.com/Textualize/textual/pull/5427
4646

47+
### Changed
48+
49+
- OptionList no longer supports `Separator`, a separator may be specified with `None`
50+
51+
### Removed
52+
53+
- Removed `wrap` argument from OptionList (use CSS `text-wrap: nowrap; text-overflow: ellipses`)
54+
- Removed `tooltip` argument from OptionList. Use `tooltip` attribute or `with_tooltip(...)` method.
4755

4856
## [1.0.0] - 2024-12-12
4957

docs/examples/widgets/option_list_options.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from textual.app import App, ComposeResult
22
from textual.widgets import Footer, Header, OptionList
3-
from textual.widgets.option_list import Option, Separator
3+
from textual.widgets.option_list import Option
44

55

66
class OptionListApp(App[None]):
@@ -11,22 +11,22 @@ def compose(self) -> ComposeResult:
1111
yield OptionList(
1212
Option("Aerilon", id="aer"),
1313
Option("Aquaria", id="aqu"),
14-
Separator(),
14+
None,
1515
Option("Canceron", id="can"),
1616
Option("Caprica", id="cap", disabled=True),
17-
Separator(),
17+
None,
1818
Option("Gemenon", id="gem"),
19-
Separator(),
19+
None,
2020
Option("Leonis", id="leo"),
2121
Option("Libran", id="lib"),
22-
Separator(),
22+
None,
2323
Option("Picon", id="pic"),
24-
Separator(),
24+
None,
2525
Option("Sagittaron", id="sag"),
2626
Option("Scorpia", id="sco"),
27-
Separator(),
27+
None,
2828
Option("Tauron", id="tau"),
29-
Separator(),
29+
None,
3030
Option("Virgon", id="vir"),
3131
)
3232
yield Footer()

src/textual/command.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535

3636
import rich.repr
3737
from rich.align import Align
38-
from rich.style import Style
3938
from rich.text import Text
4039
from typing_extensions import Final, TypeAlias
4140

@@ -48,7 +47,7 @@
4847
from textual.message import Message
4948
from textual.reactive import var
5049
from textual.screen import Screen, SystemModalScreen
51-
from textual.style import Style as VisualStyle
50+
from textual.style import Style
5251
from textual.timer import Timer
5352
from textual.types import IgnoreReturnCallbackType
5453
from textual.visual import VisualType
@@ -190,6 +189,10 @@ def __init__(self, screen: Screen[Any], match_style: Style | None = None) -> Non
190189
Args:
191190
screen: A reference to the active screen.
192191
"""
192+
if match_style is not None:
193+
assert isinstance(
194+
match_style, Style
195+
), "match_style must be a Visual style (from textual.style import Style)"
193196
self.__screen = screen
194197
self.__match_style = match_style
195198
self._init_task: Task | None = None
@@ -229,7 +232,9 @@ def matcher(self, user_input: str, case_sensitive: bool = False) -> Matcher:
229232
A [fuzzy matcher][textual.fuzzy.Matcher] object for matching against candidate hits.
230233
"""
231234
return Matcher(
232-
user_input, match_style=self.match_style, case_sensitive=case_sensitive
235+
user_input,
236+
match_style=self.match_style,
237+
case_sensitive=case_sensitive,
233238
)
234239

235240
def _post_init(self) -> None:
@@ -416,6 +421,9 @@ def __init__(
416421
self.hit = hit
417422
"""The details of the hit associated with the option."""
418423

424+
def __hash__(self) -> int:
425+
return id(self)
426+
419427
def __lt__(self, other: object) -> bool:
420428
if isinstance(other, Command):
421429
return self.hit < other.hit
@@ -803,9 +811,7 @@ def _on_mount(self, _: Mount) -> None:
803811
self.app.post_message(CommandPalette.Opened())
804812
self._calling_screen = self.app.screen_stack[-2]
805813

806-
match_style = self.get_component_rich_style(
807-
"command-palette--highlight", partial=True
808-
)
814+
match_style = self.get_visual_style("command-palette--highlight", partial=True)
809815

810816
assert self._calling_screen is not None
811817
self._providers = [
@@ -1105,9 +1111,10 @@ def build_prompt() -> Iterable[Content]:
11051111
yield Content.from_rich_text(hit.prompt)
11061112
else:
11071113
yield Content.from_markup(hit.prompt)
1114+
11081115
# Optional help text
11091116
if hit.help:
1110-
help_style = VisualStyle.from_styles(
1117+
help_style = Style.from_styles(
11111118
self.get_component_styles("command-palette--help-text")
11121119
)
11131120
yield Content.from_markup(hit.help).stylize_before(help_style)

src/textual/content.py

+18-15
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ def get_height(self, rules: RulesMap, width: int) -> int:
396396
lines = self.without_spans._wrap_and_format(
397397
width,
398398
overflow=rules.get("text_overflow", "fold"),
399-
no_wrap=rules.get("text_wrap") == "nowrap",
399+
no_wrap=rules.get("text_wrap", "wrap") == "nowrap",
400400
)
401401
return len(lines)
402402

@@ -442,17 +442,17 @@ def get_span(y: int) -> tuple[int, int] | None:
442442

443443
line = line.expand_tabs(tab_size)
444444

445-
if no_wrap and overflow == "fold":
446-
cuts = list(range(0, line.cell_length, width))[1:]
447-
new_lines = [
448-
_FormattedLine(line, width, y=y, align=align)
449-
for line in line.divide(cuts)
450-
]
451-
elif no_wrap:
452-
if overflow == "ellipsis" and no_wrap:
453-
line = line.truncate(width, ellipsis=True)
454-
content_line = _FormattedLine(line, width, y=y, align=align)
455-
new_lines = [content_line]
445+
if no_wrap:
446+
if overflow == "fold":
447+
cuts = list(range(0, line.cell_length, width))[1:]
448+
new_lines = [
449+
_FormattedLine(line, width, y=y, align=align)
450+
for line in line.divide(cuts)
451+
]
452+
else:
453+
line = line.truncate(width, ellipsis=overflow == "ellipsis")
454+
content_line = _FormattedLine(line, width, y=y, align=align)
455+
new_lines = [content_line]
456456
else:
457457
content_line = _FormattedLine(line, width, y=y, align=align)
458458
offsets = divide_line(line.plain, width, fold=overflow == "fold")
@@ -495,6 +495,7 @@ def render_strips(
495495
Returns:
496496
An list of Strips.
497497
"""
498+
498499
if not width:
499500
return []
500501

@@ -948,7 +949,7 @@ def render(
948949
self,
949950
base_style: Style = Style.null(),
950951
end: str = "\n",
951-
parse_style: Callable[[str], Style] | None = None,
952+
parse_style: Callable[[str | Style], Style] | None = None,
952953
) -> Iterable[tuple[str, Style]]:
953954
"""Render Content in to an iterable of strings and styles.
954955
@@ -971,11 +972,13 @@ def render(
971972
yield end, base_style
972973
return
973974

974-
get_style: Callable[[str], Style]
975+
get_style: Callable[[str | Style], Style]
975976
if parse_style is None:
976977

977-
def get_style(style: str, /) -> Style:
978+
def get_style(style: str | Style) -> Style:
978979
"""The default get_style method."""
980+
if isinstance(style, Style):
981+
return style
979982
try:
980983
visual_style = Style.parse(style)
981984
except Exception:

src/textual/css/stylesheet.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def set_variables(self, variables: dict[str, str]) -> None:
220220
self._parse_cache.clear()
221221
self._style_parse_cache.clear()
222222

223-
def parse_style(self, style_text: str) -> Style:
223+
def parse_style(self, style_text: str | Style) -> Style:
224224
"""Parse a (visual) Style.
225225
226226
Args:
@@ -229,6 +229,8 @@ def parse_style(self, style_text: str) -> Style:
229229
Returns:
230230
New Style instance.
231231
"""
232+
if isinstance(style_text, Style):
233+
return style_text
232234
if style_text in self._style_parse_cache:
233235
return self._style_parse_cache[style_text]
234236
style = parse_style(style_text)

src/textual/fuzzy.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
from typing import Iterable, NamedTuple
1313

1414
import rich.repr
15-
from rich.style import Style
16-
from rich.text import Text
15+
16+
from textual.content import Content
17+
from textual.visual import Style
1718

1819

1920
class _Search(NamedTuple):
@@ -203,7 +204,7 @@ def match(self, candidate: str) -> float:
203204
"""
204205
return self.fuzzy_search.match(self.query, candidate)[0]
205206

206-
def highlight(self, candidate: str) -> Text:
207+
def highlight(self, candidate: str) -> Content:
207208
"""Highlight the candidate with the fuzzy match.
208209
209210
Args:
@@ -212,11 +213,11 @@ def highlight(self, candidate: str) -> Text:
212213
Returns:
213214
A [rich.text.Text][`Text`] object with highlighted matches.
214215
"""
215-
text = Text.from_markup(candidate)
216+
content = Content.from_markup(candidate)
216217
score, offsets = self.fuzzy_search.match(self.query, candidate)
217218
if not score:
218-
return text
219+
return content
219220
for offset in offsets:
220221
if not candidate[offset].isspace():
221-
text.stylize(self._match_style, offset, offset + 1)
222-
return text
222+
content = content.stylize(self._match_style, offset, offset + 1)
223+
return content

src/textual/screen.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -962,9 +962,7 @@ def _update_focus_styles(
962962
widgets.update(widget.walk_children(with_self=True))
963963
break
964964
if widgets:
965-
self.app.stylesheet.update_nodes(
966-
[widget for widget in widgets if widget._has_focus_within], animate=True
967-
)
965+
self.app.stylesheet.update_nodes(widgets, animate=True)
968966

969967
def set_focus(
970968
self,

src/textual/strip.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from __future__ import annotations
99

1010
from itertools import chain
11-
from typing import Iterable, Iterator, Sequence
11+
from typing import Any, Iterable, Iterator, Sequence
1212

1313
import rich.repr
1414
from rich.cells import cell_len, set_cell_size
@@ -599,6 +599,19 @@ def apply_style(self, style: Style) -> Strip:
599599
self._style_cache[style] = styled_strip
600600
return styled_strip
601601

602+
def apply_meta(self, meta: dict[str, Any]) -> Strip:
603+
"""Apply meta to all segments.
604+
605+
Args:
606+
meta: A dict of meta information.
607+
608+
Returns:
609+
A new strip.
610+
611+
"""
612+
meta_style = Style.from_meta(meta)
613+
return self.apply_style(meta_style)
614+
602615
def _apply_link_style(self, link_style: Style) -> Strip:
603616
segments = self._segments
604617
_Segment = Segment

src/textual/style.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,14 @@ def __add__(self, other: object | None) -> Style:
177177
new_style = Style(
178178
(
179179
other.background
180-
if self.background is None
180+
if (self.background is None or self.background.a == 0)
181181
else self.background + other.background
182182
),
183-
self.foreground if other.foreground is None else other.foreground,
183+
(
184+
self.foreground
185+
if (other.foreground is None or other.foreground.a == 0)
186+
else other.foreground
187+
),
184188
self.bold if other.bold is None else other.bold,
185189
self.dim if other.dim is None else other.dim,
186190
self.italic if other.italic is None else other.italic,
@@ -368,6 +372,7 @@ def without_color(self) -> Style:
368372
bold=self.bold,
369373
dim=self.dim,
370374
italic=self.italic,
375+
underline=self.underline,
371376
reverse=self.reverse,
372377
strike=self.strike,
373378
link=self.link,

src/textual/types.py

-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from textual.widgets._input import InputValidationOn
2121
from textual.widgets._option_list import (
2222
DuplicateID,
23-
NewOptionListContent,
2423
OptionDoesNotExist,
2524
OptionListContent,
2625
)
@@ -41,7 +40,6 @@
4140
"IgnoreReturnCallbackType",
4241
"InputValidationOn",
4342
"MessageTarget",
44-
"NewOptionListContent",
4543
"NoActiveAppError",
4644
"NoSelection",
4745
"OptionDoesNotExist",

0 commit comments

Comments
 (0)