style and components fixes

This commit is contained in:
Will McGugan
2025-02-10 20:02:43 +00:00
parent 1cfd33c76e
commit 661ffd13ce
11 changed files with 246 additions and 159 deletions

View File

@@ -1,6 +1,6 @@
from textual.app import App, ComposeResult
from textual.widgets import Footer, Header, OptionList
from textual.widgets.option_list import Option, Separator
from textual.widgets.option_list import Option
class OptionListApp(App[None]):
@@ -11,22 +11,22 @@ class OptionListApp(App[None]):
yield OptionList(
Option("Aerilon", id="aer"),
Option("Aquaria", id="aqu"),
Separator(),
None,
Option("Canceron", id="can"),
Option("Caprica", id="cap", disabled=True),
Separator(),
None,
Option("Gemenon", id="gem"),
Separator(),
None,
Option("Leonis", id="leo"),
Option("Libran", id="lib"),
Separator(),
None,
Option("Picon", id="pic"),
Separator(),
None,
Option("Sagittaron", id="sag"),
Option("Scorpia", id="sco"),
Separator(),
None,
Option("Tauron", id="tau"),
Separator(),
None,
Option("Virgon", id="vir"),
)
yield Footer()

View File

@@ -35,7 +35,6 @@ from typing import (
import rich.repr
from rich.align import Align
from rich.style import Style
from rich.text import Text
from typing_extensions import Final, TypeAlias
@@ -48,7 +47,7 @@ from textual.fuzzy import Matcher
from textual.message import Message
from textual.reactive import var
from textual.screen import Screen, SystemModalScreen
from textual.style import Style as VisualStyle
from textual.style import Style
from textual.timer import Timer
from textual.types import IgnoreReturnCallbackType
from textual.visual import VisualType
@@ -190,6 +189,10 @@ class Provider(ABC):
Args:
screen: A reference to the active screen.
"""
if match_style is not None:
assert isinstance(
match_style, Style
), "match_style must be a Visual style if given"
self.__screen = screen
self.__match_style = match_style
self._init_task: Task | None = None
@@ -228,8 +231,13 @@ class Provider(ABC):
Returns:
A [fuzzy matcher][textual.fuzzy.Matcher] object for matching against candidate hits.
"""
match_style = self.match_style
# match_style = Style(bold=True, underline=True)
return Matcher(
user_input, match_style=self.match_style, case_sensitive=case_sensitive
user_input,
match_style=match_style,
case_sensitive=case_sensitive,
)
def _post_init(self) -> None:
@@ -806,9 +814,7 @@ class CommandPalette(SystemModalScreen[None]):
self.app.post_message(CommandPalette.Opened())
self._calling_screen = self.app.screen_stack[-2]
match_style = self.get_component_rich_style(
"command-palette--highlight", partial=True
)
match_style = self.get_visual_style("command-palette--highlight", partial=True)
assert self._calling_screen is not None
self._providers = [
@@ -1108,9 +1114,10 @@ class CommandPalette(SystemModalScreen[None]):
yield Content.from_rich_text(hit.prompt)
else:
yield Content.from_markup(hit.prompt)
# Optional help text
if hit.help:
help_style = VisualStyle.from_styles(
help_style = Style.from_styles(
self.get_component_styles("command-palette--help-text")
)
yield Content.from_markup(hit.help).stylize_before(help_style)

View File

@@ -948,7 +948,7 @@ class Content(Visual):
self,
base_style: Style = Style.null(),
end: str = "\n",
parse_style: Callable[[str], Style] | None = None,
parse_style: Callable[[str | Style], Style] | None = None,
) -> Iterable[tuple[str, Style]]:
"""Render Content in to an iterable of strings and styles.
@@ -971,11 +971,13 @@ class Content(Visual):
yield end, base_style
return
get_style: Callable[[str], Style]
get_style: Callable[[str | Style], Style]
if parse_style is None:
def get_style(style: str, /) -> Style:
def get_style(style: str | Style) -> Style:
"""The default get_style method."""
if isinstance(style, Style):
return style
try:
visual_style = Style.parse(style)
except Exception:

View File

@@ -220,7 +220,7 @@ class Stylesheet:
self._parse_cache.clear()
self._style_parse_cache.clear()
def parse_style(self, style_text: str) -> Style:
def parse_style(self, style_text: str | Style) -> Style:
"""Parse a (visual) Style.
Args:
@@ -229,6 +229,8 @@ class Stylesheet:
Returns:
New Style instance.
"""
if isinstance(style_text, Style):
return style_text
if style_text in self._style_parse_cache:
return self._style_parse_cache[style_text]
style = parse_style(style_text)

View File

@@ -12,8 +12,9 @@ from re import IGNORECASE, escape, finditer, search
from typing import Iterable, NamedTuple
import rich.repr
from rich.style import Style
from rich.text import Text
from textual.content import Content
from textual.visual import Style
class _Search(NamedTuple):
@@ -203,7 +204,7 @@ class Matcher:
"""
return self.fuzzy_search.match(self.query, candidate)[0]
def highlight(self, candidate: str) -> Text:
def highlight(self, candidate: str) -> Content:
"""Highlight the candidate with the fuzzy match.
Args:
@@ -212,11 +213,11 @@ class Matcher:
Returns:
A [rich.text.Text][`Text`] object with highlighted matches.
"""
text = Text.from_markup(candidate)
content = Content.from_markup(candidate)
score, offsets = self.fuzzy_search.match(self.query, candidate)
if not score:
return text
return content
for offset in offsets:
if not candidate[offset].isspace():
text.stylize(self._match_style, offset, offset + 1)
return text
content = content.stylize(self._match_style, offset, offset + 1)
return content

View File

@@ -177,10 +177,14 @@ class Style:
new_style = Style(
(
other.background
if self.background is None
if (self.background is None or self.background.a == 0)
else self.background + other.background
),
self.foreground if other.foreground is None else other.foreground,
(
self.foreground
if (other.foreground is None or other.foreground.a == 0)
else other.foreground
),
self.bold if other.bold is None else other.bold,
self.dim if other.dim is None else other.dim,
self.italic if other.italic is None else other.italic,
@@ -368,6 +372,7 @@ class Style:
bold=self.bold,
dim=self.dim,
italic=self.italic,
underline=self.underline,
reverse=self.reverse,
strike=self.strike,
link=self.link,

View File

@@ -1044,17 +1044,20 @@ class Widget(DOMNode):
return partial_style if partial else style
def get_visual_style(self, *component_classes: str) -> VisualStyle:
def get_visual_style(
self, *component_classes: str, partial: bool = False
) -> VisualStyle:
"""Get the visual style for the widget, including any component styles.
Args:
component_classes: Optional component styles.
partial: Return a partial style (not combined with parent).
Returns:
A Visual style instance.
"""
cache_key = (self._pseudo_classes_cache_key, component_classes)
cache_key = (self._pseudo_classes_cache_key, component_classes, partial)
if (visual_style := self._visual_style_cache.get(cache_key, None)) is None:
background = Color(0, 0, 0, 0)
color = Color(255, 255, 255, 0)
@@ -1064,6 +1067,9 @@ class Widget(DOMNode):
def iter_styles() -> Iterable[StylesBase]:
"""Iterate over the styles from the DOM and additional components styles."""
if partial:
node = self
else:
for node in reversed(self.ancestors_with_self):
yield node.styles
for name in component_classes:
@@ -4221,7 +4227,7 @@ class Widget(DOMNode):
"""
self.app.capture_mouse(None)
def select_all(self) -> None:
def text_select_all(self) -> None:
"""Select the entire widget."""
self.screen._select_all_in_widget(self)
@@ -4286,9 +4292,9 @@ class Widget(DOMNode):
async def _on_click(self, event: events.Click) -> None:
if event.widget is self:
if event.chain == 2:
self.select_all()
self.text_select_all()
elif event.chain == 3 and self.parent is not None:
self.select_container.select_all()
self.select_container.text_select_all()
await self.broker_event("click", event)

View File

@@ -11,7 +11,7 @@ from textual._loop import loop_last
from textual.binding import Binding, BindingType
from textual.cache import LRUCache
from textual.css.styles import RulesMap
from textual.geometry import Region, Size
from textual.geometry import Region, Size, clamp
from textual.message import Message
from textual.reactive import reactive
from textual.scroll_view import ScrollView
@@ -57,7 +57,7 @@ class Option:
self._prompt = prompt
self._visual: Visual | None = None
self._id = id
self._disabled = disabled
self.disabled = disabled
self._divider = False
@property
@@ -70,9 +70,15 @@ class Option:
"""Optional ID for the option."""
return self._id
@property
def disabled(self) -> bool:
return self._disabled
def _set_prompt(self, prompt: VisualType) -> None:
"""Update the prompt.
Args:
prompt: New prompt.
"""
self._prompt = prompt
self._visual = None
def __hash__(self) -> int:
return id(self)
@@ -180,7 +186,7 @@ class OptionList(ScrollView, can_focus=True):
highlighted: reactive[int | None] = reactive(None)
"""The index of the currently-highlighted option, or `None` if no option is highlighted."""
hover_option_index: reactive[int | None] = reactive(None)
_mouse_hovering_over: reactive[int | None] = reactive(None)
"""The index of the option under the mouse or `None`."""
class OptionMessage(Message):
@@ -247,14 +253,12 @@ class OptionList(ScrollView, can_focus=True):
tooltip: VisualType | None = None,
):
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
self._option_id = 0
self._wrap = wrap
self._markup = markup
self._options: list[Option] = []
self._id_to_option: dict[str, Option] = {}
self._option_to_index: dict[Option, int] = {}
self._visuals: dict[int, Visual] = {}
self._option_render_cache: LRUCache[tuple[Option, Style], list[Strip]]
self._option_render_cache = LRUCache(maxsize=1024)
@@ -264,6 +268,9 @@ class OptionList(ScrollView, can_focus=True):
self.tooltip = tooltip
self.add_options(content)
if self._options:
# TODO: Inherited from previous version. Do we always want this?
self.action_first()
@property
def options(self) -> Sequence[Option]:
@@ -285,8 +292,11 @@ class OptionList(ScrollView, can_focus=True):
Returns:
The `OptionList` instance.
"""
self._options.clear()
self._line_cache.clear()
self._option_render_cache.clear()
self._id_to_option.clear()
self._option_to_index.clear()
self.highlighted = None
self.refresh()
return self
@@ -298,6 +308,16 @@ class OptionList(ScrollView, can_focus=True):
new_options: Content of new options.
"""
option_ids = [
option._id
for option in new_options
if isinstance(option, Option) and option._id is not None
]
if len(option_ids) != len(set(option_ids)):
raise DuplicateID(
"New options contain duplicated IDs; Ensure that the IDs are unique."
)
options = self._options
add_option = self._options.append
for prompt in new_options:
@@ -309,17 +329,15 @@ class OptionList(ScrollView, can_focus=True):
continue
else:
option = Option(prompt)
hash(option)
self._option_to_index[option] = len(options)
add_option(option)
if option._id is not None:
if option._id in self._id_to_option:
raise DuplicateID("Unable to add {option!r} due to duplicate ID")
raise DuplicateID(f"Unable to add {option!r} due to duplicate ID")
self._id_to_option[option._id] = option
add_option(option)
return self
def add_option(self, option: Option | VisualType | None) -> Self:
def add_option(self, option: Option | VisualType | None = None) -> Self:
"""Add a new option to the end of the option list.
Args:
@@ -397,7 +415,7 @@ class OptionList(ScrollView, can_focus=True):
Returns:
The `OptionList` instance.
"""
self._options[index]._disabled = disabled
self._options[index].disabled = disabled
if index == self.highlighted:
self.highlighted = _widget_navigation.find_next_enabled(
self._options, anchor=index, direction=1
@@ -480,6 +498,8 @@ class OptionList(ScrollView, can_focus=True):
"""
index = self._option_to_index[option]
self._mouse_hovering_over = None
self._pre_remove_option(option, index)
for option in self.options[index + 1 :]:
current_index = self._option_to_index[option]
self._option_to_index[option] = current_index - 1
@@ -489,9 +509,19 @@ class OptionList(ScrollView, can_focus=True):
if option._id is not None:
del self._id_to_option[option._id]
del self._option_to_index[option]
self.highlighted = self.highlighted
self.refresh()
return self
def _pre_remove_option(self, option: Option, index: int) -> None:
"""Hook called prior to removing an option.
Args:
option: Option being removed.
index: Index of option being removed.
"""
def remove_option(self, option_id: str) -> Self:
"""Remove the option with the given ID.
@@ -519,9 +549,59 @@ class OptionList(ScrollView, can_focus=True):
Raises:
OptionDoesNotExist: If there is no option with the given index.
"""
try:
option = self._options[index]
except IndexError:
raise OptionDoesNotExist(
f"Unable to remove; there is no option at index {index}"
) from None
return self._remove_option(option)
def _replace_option_prompt(self, index: int, prompt: VisualType) -> None:
"""Replace the prompt of an option in the list.
Args:
index: The index of the option to replace the prompt of.
prompt: The new prompt for the option.
Raises:
OptionDoesNotExist: If there is no option with the given index.
"""
self.get_option_at_index(index)._set_prompt(prompt)
self._clear_caches()
def replace_option_prompt(self, option_id: str, prompt: VisualType) -> Self:
"""Replace the prompt of the option with the given ID.
Args:
option_id: The ID of the option to replace the prompt of.
prompt: The new prompt for the option.
Returns:
The `OptionList` instance.
Raises:
OptionDoesNotExist: If no option has the given ID.
"""
self._replace_option_prompt(self.get_option_index(option_id), prompt)
return self
def replace_option_prompt_at_index(self, index: int, prompt: VisualType) -> Self:
"""Replace the prompt of the option at the given index.
Args:
index: The index of the option to replace the prompt of.
prompt: The new prompt for the option.
Returns:
The `OptionList` instance.
Raises:
OptionDoesNotExist: If there is no option with the given index.
"""
self._replace_option_prompt(index, prompt)
return self
@property
def _lines(self) -> Sequence[tuple[int, int]]:
"""A sequence of pairs of ints for each line, used internally.
@@ -549,6 +629,7 @@ class OptionList(ScrollView, can_focus=True):
def _clear_caches(self) -> None:
self._option_render_cache.clear()
self._line_cache.clear()
self.refresh()
def notify_style_update(self) -> None:
self._clear_caches()
@@ -557,6 +638,9 @@ class OptionList(ScrollView, can_focus=True):
self._clear_caches()
self.refresh()
def _on_mount(self) -> None:
self._update_lines()
def on_show(self) -> None:
self.scroll_to_highlight()
@@ -567,11 +651,7 @@ class OptionList(ScrollView, can_focus=True):
event: The click event.
"""
clicked_option: int | None = event.style.meta.get("option")
if (
clicked_option is not None
and clicked_option >= 0
and not self._options[clicked_option].disabled
):
if clicked_option is not None and not self._options[clicked_option].disabled:
self.highlighted = clicked_option
self.action_select()
@@ -589,7 +669,7 @@ class OptionList(ScrollView, can_focus=True):
Args:
event: The mouse movement event.
"""
self.hover_option_index = event.style.meta.get("option")
self._mouse_hovering_over = event.style.meta.get("option")
def _on_leave(self, _: events.Leave) -> None:
"""React to the mouse leaving the widget."""
@@ -633,7 +713,7 @@ class OptionList(ScrollView, can_focus=True):
A list of strips.
"""
padding = self.get_component_styles("option-list--option").padding
width = self.content_region.width - self._get_left_gutter_width()
width = self.scrollable_content_region.width - self._get_left_gutter_width()
cache_key = (option, style)
if (strips := self._option_render_cache.get(cache_key)) is None:
visual = self._get_visual(option)
@@ -657,14 +737,16 @@ class OptionList(ScrollView, can_focus=True):
def _update_lines(self) -> None:
line_cache = self._line_cache
lines = line_cache.lines
last_index = lines[-1][0] if lines else 0
if not self.options:
return
next_index = lines[-1][0] + 1 if lines else 0
get_visual = self._get_visual
width = self.scrollable_content_region.width
if last_index < len(self.options) - 1:
if next_index < len(self.options):
styles = self.get_component_styles("option-list--option")
for index, option in enumerate(self.options[last_index:], last_index):
for index, option in enumerate(self.options[next_index:], next_index):
line_cache.index_to_line[index] = len(line_cache.lines)
line_count = (
get_visual(option).get_height(styles, width) + option._divider
@@ -716,6 +798,10 @@ class OptionList(ScrollView, can_focus=True):
strips = self._get_option_render(option, style)
return strips[line_offset]
def render_lines(self, crop: Region) -> list[Strip]:
self._update_lines()
return super().render_lines(crop)
def render_line(self, y: int) -> Strip:
line_number = self.scroll_offset.y + y
try:
@@ -723,25 +809,20 @@ class OptionList(ScrollView, can_focus=True):
except IndexError:
return Strip.blank(self.scrollable_content_region.width)
option = self.options[option_index]
mouse_over = self.hover_option_index == option_index
component_classes: tuple[str, ...] = ("option-list--option",)
mouse_over = self._mouse_hovering_over == option_index
component_class = ""
if option.disabled:
component_classes = (
"option-list--option",
"option-list--option-disabled",
)
component_class = "option-list--option-disabled"
elif self.highlighted == option_index:
component_classes = (
"option-list--option",
"option-list--option-highlighted",
)
component_class = "option-list--option-highlighted"
elif mouse_over:
component_classes = (
"option-list--option",
"option-list--option-hover",
)
component_class = "option-list--option-hover"
if component_class:
style = self.get_visual_style("option-list--option", component_class)
else:
style = self.get_visual_style("option-list--option")
style = self.get_visual_style(*component_classes)
strips = self._get_option_render(option, style)
strip = strips[line_offset]
return strip
@@ -760,23 +841,28 @@ class OptionList(ScrollView, can_focus=True):
"""React to the highlighted option having changed."""
if highlighted is None:
return
if not self._options[highlighted].disabled:
self.scroll_to_highlight()
option: Option
if highlighted is None:
option = None
else:
option = self.options[highlighted]
self.post_message(self.OptionHighlighted(self, option, highlighted))
self.post_message(
self.OptionHighlighted(self, self.options[highlighted], highlighted)
)
def scroll_to_highlight(self, top: bool = False) -> None:
"""Scroll to the highlighted option.
Args:
top: Ensure highlighted option is at the top of the widget.
"""
highlighted = self.highlighted
if highlighted is None or not self.is_mounted:
return
self._update_lines()
try:
y = self._index_to_line[highlighted]
except KeyError:
return
option = self.options[highlighted]
height = self._heights[highlighted] - option._divider
@@ -820,32 +906,35 @@ class OptionList(ScrollView, can_focus=True):
Args:
direction: `-1` to move up a page, `1` to move down a page.
"""
if not self._options:
return
height = self.content_region.height
option_index = self.highlighted or 0
y = min(
self._index_to_line[option_index] + direction * height,
height = self.scrollable_content_region.height
y = clamp(
self._index_to_line[self.highlighted or 0] + direction * height,
0,
len(self._lines) - 1,
)
option_index = self._lines[y][0]
target_option = _widget_navigation.find_next_enabled_no_wrap(
self.highlighted = _widget_navigation.find_next_enabled_no_wrap(
candidates=self._options,
anchor=option_index,
direction=direction,
with_anchor=True,
)
if target_option is not None:
self.highlighted = target_option
def action_page_up(self):
"""Move the highlight up one page."""
if self.highlighted is None:
self.action_first()
else:
self._move_page(-1)
def action_page_down(self):
"""Move the highlight down one page."""
if self.highlighted is None:
self.action_last()
else:
self._move_page(1)
def action_select(self) -> None:
@@ -854,34 +943,9 @@ class OptionList(ScrollView, can_focus=True):
If an option is selected then a
[OptionList.OptionSelected][textual.widgets.OptionList.OptionSelected] will be posted.
"""
if self.highlighted is None:
return
highlighted = self.highlighted
if highlighted is None:
return
option = self._options[highlighted]
if highlighted is not None and not option.disabled:
self.post_message(self.OptionSelected(self, option, highlighted))
if __name__ == "__main__":
from textual.app import App, ComposeResult
TEXT = """I must not fear.
Fear is the [u]mind-killer[/u].
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""
class OLApp(App):
def compose(self) -> ComposeResult:
yield OptionList(
*(
["Hello", "World!", None, TEXT, None, "Foo", "Bar", "Baz", None]
* 100
)
)
app = OLApp()
app.run()

View File

@@ -15,7 +15,12 @@ from textual import events
from textual.binding import Binding
from textual.messages import Message
from textual.strip import Strip
from textual.widgets._option_list import Option, OptionList, OptionListContent
from textual.widgets._option_list import (
Option,
OptionDoesNotExist,
OptionList,
OptionListContent,
)
from textual.widgets._toggle_button import ToggleButton
SelectionType = TypeVar("SelectionType")
@@ -229,6 +234,10 @@ class SelectionList(Generic[SelectionType], OptionList):
self._send_messages = False
"""Keep track of when we're ready to start sending messages."""
options = [self._make_selection(selection) for selection in selections]
self._values: dict[SelectionType, int] = {
option.value: index for index, option in enumerate(options)
}
"""Keeps track of which value relates to which option."""
super().__init__(
*options,
name=name,
@@ -237,10 +246,6 @@ class SelectionList(Generic[SelectionType], OptionList):
disabled=disabled,
wrap=False,
)
self._values: dict[SelectionType, int] = {
option.value: index for index, option in enumerate(options)
}
"""Keeps track of which value relates to which option."""
@property
def selected(self) -> list[SelectionType]:
@@ -510,20 +515,25 @@ class SelectionList(Generic[SelectionType], OptionList):
A [`Strip`][textual.strip.Strip] that is the line to render.
"""
# First off, get the underlying prompt from OptionList.
prompt = super().render_line(y)
# TODO: This is rather crufty and hard to fathom. Candidate for a rewrite.
# If it looks like the prompt itself is actually an empty line...
if not prompt:
# ...get out with that. We don't need to do any more here.
return prompt
# First off, get the underlying prompt from OptionList.
line = super().render_line(y)
# # If it looks like the prompt itself is actually an empty line...
# if not prompt:
# # ...get out with that. We don't need to do any more here.
# return prompt
# We know the prompt we're going to display, what we're going to do
# is place a CheckBox-a-like button next to it. So to start with
# let's pull out the actual Selection we're looking at right now.
_, scroll_y = self.scroll_offset
selection_index = scroll_y + y
try:
selection = self.get_option_at_index(selection_index)
except OptionDoesNotExist:
return line
# Figure out which component style is relevant for a checkbox on
# this particular line.
@@ -533,10 +543,12 @@ class SelectionList(Generic[SelectionType], OptionList):
if self.highlighted == selection_index:
component_style += "-highlighted"
# Get the underlying style used for the prompt.
underlying_style = next(iter(prompt)).style
# # # Get the underlying style used for the prompt.
underlying_style = next(iter(line)).style or self.rich_style
assert underlying_style is not None
# underlying_style = self.rich_style
# Get the style for the button.
button_style = self.get_component_rich_style(component_style)
@@ -565,7 +577,7 @@ class SelectionList(Generic[SelectionType], OptionList):
Segment(ToggleButton.BUTTON_INNER, style=button_style),
Segment(ToggleButton.BUTTON_RIGHT, style=side_style),
Segment(" ", style=underlying_style),
*prompt,
*line,
]
)
@@ -617,24 +629,17 @@ class SelectionList(Generic[SelectionType], OptionList):
"""
return cast("Selection[SelectionType]", super().get_option(option_id))
def _remove_option(self, index: int) -> None:
"""Remove a selection option from the selection option list.
Args:
index: The index of the selection option to remove.
Raises:
IndexError: If there is no selection option of the given index.
"""
option = self.get_option_at_index(index)
def _pre_remove_option(self, option: Option, index: int) -> None:
"""Hook called prior to removing an option."""
assert isinstance(option, Selection)
self._deselect(option.value)
del self._values[option.value]
# Decrement index of options after the one we just removed.
self._values = {
option_value: option_index - 1 if option_index > index else option_index
for option_value, option_index in self._values.items()
}
return super()._remove_option(index)
def add_options(
self,

View File

@@ -6,12 +6,7 @@ import pytest
from textual.app import App, ComposeResult
from textual.widgets import OptionList
from textual.widgets.option_list import (
DuplicateID,
Option,
OptionDoesNotExist,
Separator,
)
from textual.widgets.option_list import DuplicateID, Option, OptionDoesNotExist
class OptionListApp(App[None]):
@@ -21,7 +16,7 @@ class OptionListApp(App[None]):
yield OptionList(
"0",
Option("1"),
Separator(),
None,
Option("2", disabled=True),
None,
Option("3", id="3"),

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from textual import on
from textual.app import App, ComposeResult
from textual.widgets import OptionList
from textual.widgets.option_list import Option, Separator
from textual.widgets.option_list import Option
class OptionListApp(App[None]):
@@ -12,7 +12,7 @@ class OptionListApp(App[None]):
def compose(self) -> ComposeResult:
yield OptionList(
Option("0"),
Separator(),
None,
Option("1"),
)