mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
style and components fixes
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.widgets import Footer, Header, OptionList
|
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]):
|
class OptionListApp(App[None]):
|
||||||
@@ -11,22 +11,22 @@ class OptionListApp(App[None]):
|
|||||||
yield OptionList(
|
yield OptionList(
|
||||||
Option("Aerilon", id="aer"),
|
Option("Aerilon", id="aer"),
|
||||||
Option("Aquaria", id="aqu"),
|
Option("Aquaria", id="aqu"),
|
||||||
Separator(),
|
None,
|
||||||
Option("Canceron", id="can"),
|
Option("Canceron", id="can"),
|
||||||
Option("Caprica", id="cap", disabled=True),
|
Option("Caprica", id="cap", disabled=True),
|
||||||
Separator(),
|
None,
|
||||||
Option("Gemenon", id="gem"),
|
Option("Gemenon", id="gem"),
|
||||||
Separator(),
|
None,
|
||||||
Option("Leonis", id="leo"),
|
Option("Leonis", id="leo"),
|
||||||
Option("Libran", id="lib"),
|
Option("Libran", id="lib"),
|
||||||
Separator(),
|
None,
|
||||||
Option("Picon", id="pic"),
|
Option("Picon", id="pic"),
|
||||||
Separator(),
|
None,
|
||||||
Option("Sagittaron", id="sag"),
|
Option("Sagittaron", id="sag"),
|
||||||
Option("Scorpia", id="sco"),
|
Option("Scorpia", id="sco"),
|
||||||
Separator(),
|
None,
|
||||||
Option("Tauron", id="tau"),
|
Option("Tauron", id="tau"),
|
||||||
Separator(),
|
None,
|
||||||
Option("Virgon", id="vir"),
|
Option("Virgon", id="vir"),
|
||||||
)
|
)
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ from typing import (
|
|||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.align import Align
|
from rich.align import Align
|
||||||
from rich.style import Style
|
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from typing_extensions import Final, TypeAlias
|
from typing_extensions import Final, TypeAlias
|
||||||
|
|
||||||
@@ -48,7 +47,7 @@ from textual.fuzzy import Matcher
|
|||||||
from textual.message import Message
|
from textual.message import Message
|
||||||
from textual.reactive import var
|
from textual.reactive import var
|
||||||
from textual.screen import Screen, SystemModalScreen
|
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.timer import Timer
|
||||||
from textual.types import IgnoreReturnCallbackType
|
from textual.types import IgnoreReturnCallbackType
|
||||||
from textual.visual import VisualType
|
from textual.visual import VisualType
|
||||||
@@ -190,6 +189,10 @@ class Provider(ABC):
|
|||||||
Args:
|
Args:
|
||||||
screen: A reference to the active screen.
|
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.__screen = screen
|
||||||
self.__match_style = match_style
|
self.__match_style = match_style
|
||||||
self._init_task: Task | None = None
|
self._init_task: Task | None = None
|
||||||
@@ -228,8 +231,13 @@ class Provider(ABC):
|
|||||||
Returns:
|
Returns:
|
||||||
A [fuzzy matcher][textual.fuzzy.Matcher] object for matching against candidate hits.
|
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(
|
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:
|
def _post_init(self) -> None:
|
||||||
@@ -806,9 +814,7 @@ class CommandPalette(SystemModalScreen[None]):
|
|||||||
self.app.post_message(CommandPalette.Opened())
|
self.app.post_message(CommandPalette.Opened())
|
||||||
self._calling_screen = self.app.screen_stack[-2]
|
self._calling_screen = self.app.screen_stack[-2]
|
||||||
|
|
||||||
match_style = self.get_component_rich_style(
|
match_style = self.get_visual_style("command-palette--highlight", partial=True)
|
||||||
"command-palette--highlight", partial=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert self._calling_screen is not None
|
assert self._calling_screen is not None
|
||||||
self._providers = [
|
self._providers = [
|
||||||
@@ -1108,9 +1114,10 @@ class CommandPalette(SystemModalScreen[None]):
|
|||||||
yield Content.from_rich_text(hit.prompt)
|
yield Content.from_rich_text(hit.prompt)
|
||||||
else:
|
else:
|
||||||
yield Content.from_markup(hit.prompt)
|
yield Content.from_markup(hit.prompt)
|
||||||
|
|
||||||
# Optional help text
|
# Optional help text
|
||||||
if hit.help:
|
if hit.help:
|
||||||
help_style = VisualStyle.from_styles(
|
help_style = Style.from_styles(
|
||||||
self.get_component_styles("command-palette--help-text")
|
self.get_component_styles("command-palette--help-text")
|
||||||
)
|
)
|
||||||
yield Content.from_markup(hit.help).stylize_before(help_style)
|
yield Content.from_markup(hit.help).stylize_before(help_style)
|
||||||
|
|||||||
@@ -948,7 +948,7 @@ class Content(Visual):
|
|||||||
self,
|
self,
|
||||||
base_style: Style = Style.null(),
|
base_style: Style = Style.null(),
|
||||||
end: str = "\n",
|
end: str = "\n",
|
||||||
parse_style: Callable[[str], Style] | None = None,
|
parse_style: Callable[[str | Style], Style] | None = None,
|
||||||
) -> Iterable[tuple[str, Style]]:
|
) -> Iterable[tuple[str, Style]]:
|
||||||
"""Render Content in to an iterable of strings and styles.
|
"""Render Content in to an iterable of strings and styles.
|
||||||
|
|
||||||
@@ -971,11 +971,13 @@ class Content(Visual):
|
|||||||
yield end, base_style
|
yield end, base_style
|
||||||
return
|
return
|
||||||
|
|
||||||
get_style: Callable[[str], Style]
|
get_style: Callable[[str | Style], Style]
|
||||||
if parse_style is None:
|
if parse_style is None:
|
||||||
|
|
||||||
def get_style(style: str, /) -> Style:
|
def get_style(style: str | Style) -> Style:
|
||||||
"""The default get_style method."""
|
"""The default get_style method."""
|
||||||
|
if isinstance(style, Style):
|
||||||
|
return style
|
||||||
try:
|
try:
|
||||||
visual_style = Style.parse(style)
|
visual_style = Style.parse(style)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ class Stylesheet:
|
|||||||
self._parse_cache.clear()
|
self._parse_cache.clear()
|
||||||
self._style_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.
|
"""Parse a (visual) Style.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -229,6 +229,8 @@ class Stylesheet:
|
|||||||
Returns:
|
Returns:
|
||||||
New Style instance.
|
New Style instance.
|
||||||
"""
|
"""
|
||||||
|
if isinstance(style_text, Style):
|
||||||
|
return style_text
|
||||||
if style_text in self._style_parse_cache:
|
if style_text in self._style_parse_cache:
|
||||||
return self._style_parse_cache[style_text]
|
return self._style_parse_cache[style_text]
|
||||||
style = parse_style(style_text)
|
style = parse_style(style_text)
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ from re import IGNORECASE, escape, finditer, search
|
|||||||
from typing import Iterable, NamedTuple
|
from typing import Iterable, NamedTuple
|
||||||
|
|
||||||
import rich.repr
|
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):
|
class _Search(NamedTuple):
|
||||||
@@ -203,7 +204,7 @@ class Matcher:
|
|||||||
"""
|
"""
|
||||||
return self.fuzzy_search.match(self.query, candidate)[0]
|
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.
|
"""Highlight the candidate with the fuzzy match.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -212,11 +213,11 @@ class Matcher:
|
|||||||
Returns:
|
Returns:
|
||||||
A [rich.text.Text][`Text`] object with highlighted matches.
|
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)
|
score, offsets = self.fuzzy_search.match(self.query, candidate)
|
||||||
if not score:
|
if not score:
|
||||||
return text
|
return content
|
||||||
for offset in offsets:
|
for offset in offsets:
|
||||||
if not candidate[offset].isspace():
|
if not candidate[offset].isspace():
|
||||||
text.stylize(self._match_style, offset, offset + 1)
|
content = content.stylize(self._match_style, offset, offset + 1)
|
||||||
return text
|
return content
|
||||||
|
|||||||
@@ -177,10 +177,14 @@ class Style:
|
|||||||
new_style = Style(
|
new_style = Style(
|
||||||
(
|
(
|
||||||
other.background
|
other.background
|
||||||
if self.background is None
|
if (self.background is None or self.background.a == 0)
|
||||||
else self.background + other.background
|
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.bold if other.bold is None else other.bold,
|
||||||
self.dim if other.dim is None else other.dim,
|
self.dim if other.dim is None else other.dim,
|
||||||
self.italic if other.italic is None else other.italic,
|
self.italic if other.italic is None else other.italic,
|
||||||
@@ -368,6 +372,7 @@ class Style:
|
|||||||
bold=self.bold,
|
bold=self.bold,
|
||||||
dim=self.dim,
|
dim=self.dim,
|
||||||
italic=self.italic,
|
italic=self.italic,
|
||||||
|
underline=self.underline,
|
||||||
reverse=self.reverse,
|
reverse=self.reverse,
|
||||||
strike=self.strike,
|
strike=self.strike,
|
||||||
link=self.link,
|
link=self.link,
|
||||||
|
|||||||
@@ -1044,17 +1044,20 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
return partial_style if partial else style
|
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.
|
"""Get the visual style for the widget, including any component styles.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
component_classes: Optional component styles.
|
component_classes: Optional component styles.
|
||||||
|
partial: Return a partial style (not combined with parent).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A Visual style instance.
|
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:
|
if (visual_style := self._visual_style_cache.get(cache_key, None)) is None:
|
||||||
background = Color(0, 0, 0, 0)
|
background = Color(0, 0, 0, 0)
|
||||||
color = Color(255, 255, 255, 0)
|
color = Color(255, 255, 255, 0)
|
||||||
@@ -1064,6 +1067,9 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
def iter_styles() -> Iterable[StylesBase]:
|
def iter_styles() -> Iterable[StylesBase]:
|
||||||
"""Iterate over the styles from the DOM and additional components styles."""
|
"""Iterate over the styles from the DOM and additional components styles."""
|
||||||
|
if partial:
|
||||||
|
node = self
|
||||||
|
else:
|
||||||
for node in reversed(self.ancestors_with_self):
|
for node in reversed(self.ancestors_with_self):
|
||||||
yield node.styles
|
yield node.styles
|
||||||
for name in component_classes:
|
for name in component_classes:
|
||||||
@@ -4221,7 +4227,7 @@ class Widget(DOMNode):
|
|||||||
"""
|
"""
|
||||||
self.app.capture_mouse(None)
|
self.app.capture_mouse(None)
|
||||||
|
|
||||||
def select_all(self) -> None:
|
def text_select_all(self) -> None:
|
||||||
"""Select the entire widget."""
|
"""Select the entire widget."""
|
||||||
self.screen._select_all_in_widget(self)
|
self.screen._select_all_in_widget(self)
|
||||||
|
|
||||||
@@ -4286,9 +4292,9 @@ class Widget(DOMNode):
|
|||||||
async def _on_click(self, event: events.Click) -> None:
|
async def _on_click(self, event: events.Click) -> None:
|
||||||
if event.widget is self:
|
if event.widget is self:
|
||||||
if event.chain == 2:
|
if event.chain == 2:
|
||||||
self.select_all()
|
self.text_select_all()
|
||||||
elif event.chain == 3 and self.parent is not None:
|
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)
|
await self.broker_event("click", event)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from textual._loop import loop_last
|
|||||||
from textual.binding import Binding, BindingType
|
from textual.binding import Binding, BindingType
|
||||||
from textual.cache import LRUCache
|
from textual.cache import LRUCache
|
||||||
from textual.css.styles import RulesMap
|
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.message import Message
|
||||||
from textual.reactive import reactive
|
from textual.reactive import reactive
|
||||||
from textual.scroll_view import ScrollView
|
from textual.scroll_view import ScrollView
|
||||||
@@ -57,7 +57,7 @@ class Option:
|
|||||||
self._prompt = prompt
|
self._prompt = prompt
|
||||||
self._visual: Visual | None = None
|
self._visual: Visual | None = None
|
||||||
self._id = id
|
self._id = id
|
||||||
self._disabled = disabled
|
self.disabled = disabled
|
||||||
self._divider = False
|
self._divider = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -70,9 +70,15 @@ class Option:
|
|||||||
"""Optional ID for the option."""
|
"""Optional ID for the option."""
|
||||||
return self._id
|
return self._id
|
||||||
|
|
||||||
@property
|
def _set_prompt(self, prompt: VisualType) -> None:
|
||||||
def disabled(self) -> bool:
|
"""Update the prompt.
|
||||||
return self._disabled
|
|
||||||
|
Args:
|
||||||
|
prompt: New prompt.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._prompt = prompt
|
||||||
|
self._visual = None
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return id(self)
|
return id(self)
|
||||||
@@ -180,7 +186,7 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
highlighted: reactive[int | None] = reactive(None)
|
highlighted: reactive[int | None] = reactive(None)
|
||||||
"""The index of the currently-highlighted option, or `None` if no option is highlighted."""
|
"""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`."""
|
"""The index of the option under the mouse or `None`."""
|
||||||
|
|
||||||
class OptionMessage(Message):
|
class OptionMessage(Message):
|
||||||
@@ -247,14 +253,12 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
tooltip: VisualType | None = None,
|
tooltip: VisualType | None = None,
|
||||||
):
|
):
|
||||||
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
|
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
|
||||||
self._option_id = 0
|
|
||||||
self._wrap = wrap
|
self._wrap = wrap
|
||||||
self._markup = markup
|
self._markup = markup
|
||||||
self._options: list[Option] = []
|
self._options: list[Option] = []
|
||||||
self._id_to_option: dict[str, Option] = {}
|
self._id_to_option: dict[str, Option] = {}
|
||||||
self._option_to_index: dict[Option, int] = {}
|
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[tuple[Option, Style], list[Strip]]
|
||||||
self._option_render_cache = LRUCache(maxsize=1024)
|
self._option_render_cache = LRUCache(maxsize=1024)
|
||||||
|
|
||||||
@@ -264,6 +268,9 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
self.tooltip = tooltip
|
self.tooltip = tooltip
|
||||||
|
|
||||||
self.add_options(content)
|
self.add_options(content)
|
||||||
|
if self._options:
|
||||||
|
# TODO: Inherited from previous version. Do we always want this?
|
||||||
|
self.action_first()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def options(self) -> Sequence[Option]:
|
def options(self) -> Sequence[Option]:
|
||||||
@@ -285,8 +292,11 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
Returns:
|
Returns:
|
||||||
The `OptionList` instance.
|
The `OptionList` instance.
|
||||||
"""
|
"""
|
||||||
|
self._options.clear()
|
||||||
self._line_cache.clear()
|
self._line_cache.clear()
|
||||||
self._option_render_cache.clear()
|
self._option_render_cache.clear()
|
||||||
|
self._id_to_option.clear()
|
||||||
|
self._option_to_index.clear()
|
||||||
self.highlighted = None
|
self.highlighted = None
|
||||||
self.refresh()
|
self.refresh()
|
||||||
return self
|
return self
|
||||||
@@ -298,6 +308,16 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
new_options: Content of new options.
|
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
|
options = self._options
|
||||||
add_option = self._options.append
|
add_option = self._options.append
|
||||||
for prompt in new_options:
|
for prompt in new_options:
|
||||||
@@ -309,17 +329,15 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
option = Option(prompt)
|
option = Option(prompt)
|
||||||
hash(option)
|
|
||||||
self._option_to_index[option] = len(options)
|
self._option_to_index[option] = len(options)
|
||||||
add_option(option)
|
|
||||||
if option._id is not None:
|
if option._id is not None:
|
||||||
if option._id in self._id_to_option:
|
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
|
self._id_to_option[option._id] = option
|
||||||
|
add_option(option)
|
||||||
return self
|
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.
|
"""Add a new option to the end of the option list.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -397,7 +415,7 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
Returns:
|
Returns:
|
||||||
The `OptionList` instance.
|
The `OptionList` instance.
|
||||||
"""
|
"""
|
||||||
self._options[index]._disabled = disabled
|
self._options[index].disabled = disabled
|
||||||
if index == self.highlighted:
|
if index == self.highlighted:
|
||||||
self.highlighted = _widget_navigation.find_next_enabled(
|
self.highlighted = _widget_navigation.find_next_enabled(
|
||||||
self._options, anchor=index, direction=1
|
self._options, anchor=index, direction=1
|
||||||
@@ -480,6 +498,8 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
index = self._option_to_index[option]
|
index = self._option_to_index[option]
|
||||||
|
self._mouse_hovering_over = None
|
||||||
|
self._pre_remove_option(option, index)
|
||||||
for option in self.options[index + 1 :]:
|
for option in self.options[index + 1 :]:
|
||||||
current_index = self._option_to_index[option]
|
current_index = self._option_to_index[option]
|
||||||
self._option_to_index[option] = current_index - 1
|
self._option_to_index[option] = current_index - 1
|
||||||
@@ -489,9 +509,19 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
if option._id is not None:
|
if option._id is not None:
|
||||||
del self._id_to_option[option._id]
|
del self._id_to_option[option._id]
|
||||||
del self._option_to_index[option]
|
del self._option_to_index[option]
|
||||||
|
self.highlighted = self.highlighted
|
||||||
self.refresh()
|
self.refresh()
|
||||||
return self
|
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:
|
def remove_option(self, option_id: str) -> Self:
|
||||||
"""Remove the option with the given ID.
|
"""Remove the option with the given ID.
|
||||||
|
|
||||||
@@ -519,9 +549,59 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
Raises:
|
Raises:
|
||||||
OptionDoesNotExist: If there is no option with the given index.
|
OptionDoesNotExist: If there is no option with the given index.
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
option = self._options[index]
|
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)
|
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
|
@property
|
||||||
def _lines(self) -> Sequence[tuple[int, int]]:
|
def _lines(self) -> Sequence[tuple[int, int]]:
|
||||||
"""A sequence of pairs of ints for each line, used internally.
|
"""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:
|
def _clear_caches(self) -> None:
|
||||||
self._option_render_cache.clear()
|
self._option_render_cache.clear()
|
||||||
self._line_cache.clear()
|
self._line_cache.clear()
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
def notify_style_update(self) -> None:
|
def notify_style_update(self) -> None:
|
||||||
self._clear_caches()
|
self._clear_caches()
|
||||||
@@ -557,6 +638,9 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
self._clear_caches()
|
self._clear_caches()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
|
def _on_mount(self) -> None:
|
||||||
|
self._update_lines()
|
||||||
|
|
||||||
def on_show(self) -> None:
|
def on_show(self) -> None:
|
||||||
self.scroll_to_highlight()
|
self.scroll_to_highlight()
|
||||||
|
|
||||||
@@ -567,11 +651,7 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
event: The click event.
|
event: The click event.
|
||||||
"""
|
"""
|
||||||
clicked_option: int | None = event.style.meta.get("option")
|
clicked_option: int | None = event.style.meta.get("option")
|
||||||
if (
|
if clicked_option is not None and not self._options[clicked_option].disabled:
|
||||||
clicked_option is not None
|
|
||||||
and clicked_option >= 0
|
|
||||||
and not self._options[clicked_option].disabled
|
|
||||||
):
|
|
||||||
self.highlighted = clicked_option
|
self.highlighted = clicked_option
|
||||||
self.action_select()
|
self.action_select()
|
||||||
|
|
||||||
@@ -589,7 +669,7 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
Args:
|
Args:
|
||||||
event: The mouse movement event.
|
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:
|
def _on_leave(self, _: events.Leave) -> None:
|
||||||
"""React to the mouse leaving the widget."""
|
"""React to the mouse leaving the widget."""
|
||||||
@@ -633,7 +713,7 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
A list of strips.
|
A list of strips.
|
||||||
"""
|
"""
|
||||||
padding = self.get_component_styles("option-list--option").padding
|
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)
|
cache_key = (option, style)
|
||||||
if (strips := self._option_render_cache.get(cache_key)) is None:
|
if (strips := self._option_render_cache.get(cache_key)) is None:
|
||||||
visual = self._get_visual(option)
|
visual = self._get_visual(option)
|
||||||
@@ -657,14 +737,16 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
def _update_lines(self) -> None:
|
def _update_lines(self) -> None:
|
||||||
line_cache = self._line_cache
|
line_cache = self._line_cache
|
||||||
lines = line_cache.lines
|
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
|
get_visual = self._get_visual
|
||||||
width = self.scrollable_content_region.width
|
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")
|
styles = self.get_component_styles("option-list--option")
|
||||||
|
for index, option in enumerate(self.options[next_index:], next_index):
|
||||||
for index, option in enumerate(self.options[last_index:], last_index):
|
|
||||||
line_cache.index_to_line[index] = len(line_cache.lines)
|
line_cache.index_to_line[index] = len(line_cache.lines)
|
||||||
line_count = (
|
line_count = (
|
||||||
get_visual(option).get_height(styles, width) + option._divider
|
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)
|
strips = self._get_option_render(option, style)
|
||||||
return strips[line_offset]
|
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:
|
def render_line(self, y: int) -> Strip:
|
||||||
line_number = self.scroll_offset.y + y
|
line_number = self.scroll_offset.y + y
|
||||||
try:
|
try:
|
||||||
@@ -723,25 +809,20 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
return Strip.blank(self.scrollable_content_region.width)
|
return Strip.blank(self.scrollable_content_region.width)
|
||||||
option = self.options[option_index]
|
option = self.options[option_index]
|
||||||
mouse_over = self.hover_option_index == option_index
|
mouse_over = self._mouse_hovering_over == option_index
|
||||||
component_classes: tuple[str, ...] = ("option-list--option",)
|
component_class = ""
|
||||||
if option.disabled:
|
if option.disabled:
|
||||||
component_classes = (
|
component_class = "option-list--option-disabled"
|
||||||
"option-list--option",
|
|
||||||
"option-list--option-disabled",
|
|
||||||
)
|
|
||||||
elif self.highlighted == option_index:
|
elif self.highlighted == option_index:
|
||||||
component_classes = (
|
component_class = "option-list--option-highlighted"
|
||||||
"option-list--option",
|
|
||||||
"option-list--option-highlighted",
|
|
||||||
)
|
|
||||||
elif mouse_over:
|
elif mouse_over:
|
||||||
component_classes = (
|
component_class = "option-list--option-hover"
|
||||||
"option-list--option",
|
|
||||||
"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)
|
strips = self._get_option_render(option, style)
|
||||||
strip = strips[line_offset]
|
strip = strips[line_offset]
|
||||||
return strip
|
return strip
|
||||||
@@ -760,23 +841,28 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
"""React to the highlighted option having changed."""
|
"""React to the highlighted option having changed."""
|
||||||
if highlighted is None:
|
if highlighted is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self._options[highlighted].disabled:
|
if not self._options[highlighted].disabled:
|
||||||
self.scroll_to_highlight()
|
self.scroll_to_highlight()
|
||||||
|
self.post_message(
|
||||||
option: Option
|
self.OptionHighlighted(self, self.options[highlighted], highlighted)
|
||||||
if highlighted is None:
|
)
|
||||||
option = None
|
|
||||||
else:
|
|
||||||
option = self.options[highlighted]
|
|
||||||
self.post_message(self.OptionHighlighted(self, option, highlighted))
|
|
||||||
|
|
||||||
def scroll_to_highlight(self, top: bool = False) -> None:
|
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
|
highlighted = self.highlighted
|
||||||
if highlighted is None or not self.is_mounted:
|
if highlighted is None or not self.is_mounted:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._update_lines()
|
||||||
|
|
||||||
|
try:
|
||||||
y = self._index_to_line[highlighted]
|
y = self._index_to_line[highlighted]
|
||||||
|
except KeyError:
|
||||||
|
return
|
||||||
option = self.options[highlighted]
|
option = self.options[highlighted]
|
||||||
height = self._heights[highlighted] - option._divider
|
height = self._heights[highlighted] - option._divider
|
||||||
|
|
||||||
@@ -820,32 +906,35 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
Args:
|
Args:
|
||||||
direction: `-1` to move up a page, `1` to move down a page.
|
direction: `-1` to move up a page, `1` to move down a page.
|
||||||
"""
|
"""
|
||||||
|
if not self._options:
|
||||||
|
return
|
||||||
|
|
||||||
height = self.content_region.height
|
height = self.scrollable_content_region.height
|
||||||
|
y = clamp(
|
||||||
option_index = self.highlighted or 0
|
self._index_to_line[self.highlighted or 0] + direction * height,
|
||||||
|
0,
|
||||||
y = min(
|
|
||||||
self._index_to_line[option_index] + direction * height,
|
|
||||||
len(self._lines) - 1,
|
len(self._lines) - 1,
|
||||||
)
|
)
|
||||||
option_index = self._lines[y][0]
|
option_index = self._lines[y][0]
|
||||||
|
self.highlighted = _widget_navigation.find_next_enabled_no_wrap(
|
||||||
target_option = _widget_navigation.find_next_enabled_no_wrap(
|
|
||||||
candidates=self._options,
|
candidates=self._options,
|
||||||
anchor=option_index,
|
anchor=option_index,
|
||||||
direction=direction,
|
direction=direction,
|
||||||
with_anchor=True,
|
with_anchor=True,
|
||||||
)
|
)
|
||||||
if target_option is not None:
|
|
||||||
self.highlighted = target_option
|
|
||||||
|
|
||||||
def action_page_up(self):
|
def action_page_up(self):
|
||||||
"""Move the highlight up one page."""
|
"""Move the highlight up one page."""
|
||||||
|
if self.highlighted is None:
|
||||||
|
self.action_first()
|
||||||
|
else:
|
||||||
self._move_page(-1)
|
self._move_page(-1)
|
||||||
|
|
||||||
def action_page_down(self):
|
def action_page_down(self):
|
||||||
"""Move the highlight down one page."""
|
"""Move the highlight down one page."""
|
||||||
|
if self.highlighted is None:
|
||||||
|
self.action_last()
|
||||||
|
else:
|
||||||
self._move_page(1)
|
self._move_page(1)
|
||||||
|
|
||||||
def action_select(self) -> None:
|
def action_select(self) -> None:
|
||||||
@@ -854,34 +943,9 @@ class OptionList(ScrollView, can_focus=True):
|
|||||||
If an option is selected then a
|
If an option is selected then a
|
||||||
[OptionList.OptionSelected][textual.widgets.OptionList.OptionSelected] will be posted.
|
[OptionList.OptionSelected][textual.widgets.OptionList.OptionSelected] will be posted.
|
||||||
"""
|
"""
|
||||||
if self.highlighted is None:
|
|
||||||
return
|
|
||||||
highlighted = self.highlighted
|
highlighted = self.highlighted
|
||||||
|
if highlighted is None:
|
||||||
|
return
|
||||||
option = self._options[highlighted]
|
option = self._options[highlighted]
|
||||||
if highlighted is not None and not option.disabled:
|
if highlighted is not None and not option.disabled:
|
||||||
self.post_message(self.OptionSelected(self, option, highlighted))
|
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()
|
|
||||||
|
|||||||
@@ -15,7 +15,12 @@ from textual import events
|
|||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
from textual.messages import Message
|
from textual.messages import Message
|
||||||
from textual.strip import Strip
|
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
|
from textual.widgets._toggle_button import ToggleButton
|
||||||
|
|
||||||
SelectionType = TypeVar("SelectionType")
|
SelectionType = TypeVar("SelectionType")
|
||||||
@@ -229,6 +234,10 @@ class SelectionList(Generic[SelectionType], OptionList):
|
|||||||
self._send_messages = False
|
self._send_messages = False
|
||||||
"""Keep track of when we're ready to start sending messages."""
|
"""Keep track of when we're ready to start sending messages."""
|
||||||
options = [self._make_selection(selection) for selection in selections]
|
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__(
|
super().__init__(
|
||||||
*options,
|
*options,
|
||||||
name=name,
|
name=name,
|
||||||
@@ -237,10 +246,6 @@ class SelectionList(Generic[SelectionType], OptionList):
|
|||||||
disabled=disabled,
|
disabled=disabled,
|
||||||
wrap=False,
|
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
|
@property
|
||||||
def selected(self) -> list[SelectionType]:
|
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.
|
A [`Strip`][textual.strip.Strip] that is the line to render.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# First off, get the underlying prompt from OptionList.
|
# TODO: This is rather crufty and hard to fathom. Candidate for a rewrite.
|
||||||
prompt = super().render_line(y)
|
|
||||||
|
|
||||||
# If it looks like the prompt itself is actually an empty line...
|
# First off, get the underlying prompt from OptionList.
|
||||||
if not prompt:
|
line = super().render_line(y)
|
||||||
# ...get out with that. We don't need to do any more here.
|
|
||||||
return prompt
|
# # 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
|
# 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
|
# 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.
|
# let's pull out the actual Selection we're looking at right now.
|
||||||
_, scroll_y = self.scroll_offset
|
_, scroll_y = self.scroll_offset
|
||||||
selection_index = scroll_y + y
|
selection_index = scroll_y + y
|
||||||
|
try:
|
||||||
selection = self.get_option_at_index(selection_index)
|
selection = self.get_option_at_index(selection_index)
|
||||||
|
except OptionDoesNotExist:
|
||||||
|
return line
|
||||||
|
|
||||||
# Figure out which component style is relevant for a checkbox on
|
# Figure out which component style is relevant for a checkbox on
|
||||||
# this particular line.
|
# this particular line.
|
||||||
@@ -533,10 +543,12 @@ class SelectionList(Generic[SelectionType], OptionList):
|
|||||||
if self.highlighted == selection_index:
|
if self.highlighted == selection_index:
|
||||||
component_style += "-highlighted"
|
component_style += "-highlighted"
|
||||||
|
|
||||||
# Get the underlying style used for the prompt.
|
# # # Get the underlying style used for the prompt.
|
||||||
underlying_style = next(iter(prompt)).style
|
underlying_style = next(iter(line)).style or self.rich_style
|
||||||
assert underlying_style is not None
|
assert underlying_style is not None
|
||||||
|
|
||||||
|
# underlying_style = self.rich_style
|
||||||
|
|
||||||
# Get the style for the button.
|
# Get the style for the button.
|
||||||
button_style = self.get_component_rich_style(component_style)
|
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_INNER, style=button_style),
|
||||||
Segment(ToggleButton.BUTTON_RIGHT, style=side_style),
|
Segment(ToggleButton.BUTTON_RIGHT, style=side_style),
|
||||||
Segment(" ", style=underlying_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))
|
return cast("Selection[SelectionType]", super().get_option(option_id))
|
||||||
|
|
||||||
def _remove_option(self, index: int) -> None:
|
def _pre_remove_option(self, option: Option, index: int) -> None:
|
||||||
"""Remove a selection option from the selection option list.
|
"""Hook called prior to removing an option."""
|
||||||
|
assert isinstance(option, Selection)
|
||||||
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)
|
|
||||||
self._deselect(option.value)
|
self._deselect(option.value)
|
||||||
del self._values[option.value]
|
del self._values[option.value]
|
||||||
|
|
||||||
# Decrement index of options after the one we just removed.
|
# Decrement index of options after the one we just removed.
|
||||||
self._values = {
|
self._values = {
|
||||||
option_value: option_index - 1 if option_index > index else option_index
|
option_value: option_index - 1 if option_index > index else option_index
|
||||||
for option_value, option_index in self._values.items()
|
for option_value, option_index in self._values.items()
|
||||||
}
|
}
|
||||||
return super()._remove_option(index)
|
|
||||||
|
|
||||||
def add_options(
|
def add_options(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -6,12 +6,7 @@ import pytest
|
|||||||
|
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.widgets import OptionList
|
from textual.widgets import OptionList
|
||||||
from textual.widgets.option_list import (
|
from textual.widgets.option_list import DuplicateID, Option, OptionDoesNotExist
|
||||||
DuplicateID,
|
|
||||||
Option,
|
|
||||||
OptionDoesNotExist,
|
|
||||||
Separator,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OptionListApp(App[None]):
|
class OptionListApp(App[None]):
|
||||||
@@ -21,7 +16,7 @@ class OptionListApp(App[None]):
|
|||||||
yield OptionList(
|
yield OptionList(
|
||||||
"0",
|
"0",
|
||||||
Option("1"),
|
Option("1"),
|
||||||
Separator(),
|
None,
|
||||||
Option("2", disabled=True),
|
Option("2", disabled=True),
|
||||||
None,
|
None,
|
||||||
Option("3", id="3"),
|
Option("3", id="3"),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from textual import on
|
from textual import on
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.widgets import OptionList
|
from textual.widgets import OptionList
|
||||||
from textual.widgets.option_list import Option, Separator
|
from textual.widgets.option_list import Option
|
||||||
|
|
||||||
|
|
||||||
class OptionListApp(App[None]):
|
class OptionListApp(App[None]):
|
||||||
@@ -12,7 +12,7 @@ class OptionListApp(App[None]):
|
|||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield OptionList(
|
yield OptionList(
|
||||||
Option("0"),
|
Option("0"),
|
||||||
Separator(),
|
None,
|
||||||
Option("1"),
|
Option("1"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user