diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95f5112ade..ecbd5b818e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -44,6 +44,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed scrollbars ignoring background opacity https://github.com/Textualize/textual/issues/5458
- Fixed `Header` icon showing command palette tooltip when disabled https://github.com/Textualize/textual/pull/5427
+### Changed
+
+- OptionList no longer supports `Separator`, a separator may be specified with `None`
+
+### Removed
+
+- Removed `wrap` argument from OptionList (use CSS `text-wrap: nowrap; text-overflow: ellipses`)
+- Removed `tooltip` argument from OptionList. Use `tooltip` attribute or `with_tooltip(...)` method.
## [1.0.0] - 2024-12-12
diff --git a/docs/examples/widgets/option_list_options.py b/docs/examples/widgets/option_list_options.py
index 611a7ef088..fe3d2ec298 100644
--- a/docs/examples/widgets/option_list_options.py
+++ b/docs/examples/widgets/option_list_options.py
@@ -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 @@ def compose(self) -> ComposeResult:
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()
diff --git a/src/textual/command.py b/src/textual/command.py
index 0131f2c5c9..93823bb999 100644
--- a/src/textual/command.py
+++ b/src/textual/command.py
@@ -35,7 +35,6 @@
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.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 @@ def __init__(self, screen: Screen[Any], match_style: Style | None = None) -> Non
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 (from textual.style import Style)"
self.__screen = screen
self.__match_style = match_style
self._init_task: Task | None = None
@@ -229,7 +232,9 @@ def matcher(self, user_input: str, case_sensitive: bool = False) -> Matcher:
A [fuzzy matcher][textual.fuzzy.Matcher] object for matching against candidate hits.
"""
return Matcher(
- user_input, match_style=self.match_style, case_sensitive=case_sensitive
+ user_input,
+ match_style=self.match_style,
+ case_sensitive=case_sensitive,
)
def _post_init(self) -> None:
@@ -416,6 +421,9 @@ def __init__(
self.hit = hit
"""The details of the hit associated with the option."""
+ def __hash__(self) -> int:
+ return id(self)
+
def __lt__(self, other: object) -> bool:
if isinstance(other, Command):
return self.hit < other.hit
@@ -803,9 +811,7 @@ def _on_mount(self, _: Mount) -> 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 = [
@@ -1105,9 +1111,10 @@ def build_prompt() -> Iterable[Content]:
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)
diff --git a/src/textual/content.py b/src/textual/content.py
index 80667d25c0..11e99923b3 100644
--- a/src/textual/content.py
+++ b/src/textual/content.py
@@ -396,7 +396,7 @@ def get_height(self, rules: RulesMap, width: int) -> int:
lines = self.without_spans._wrap_and_format(
width,
overflow=rules.get("text_overflow", "fold"),
- no_wrap=rules.get("text_wrap") == "nowrap",
+ no_wrap=rules.get("text_wrap", "wrap") == "nowrap",
)
return len(lines)
@@ -442,17 +442,17 @@ def get_span(y: int) -> tuple[int, int] | None:
line = line.expand_tabs(tab_size)
- if no_wrap and overflow == "fold":
- cuts = list(range(0, line.cell_length, width))[1:]
- new_lines = [
- _FormattedLine(line, width, y=y, align=align)
- for line in line.divide(cuts)
- ]
- elif no_wrap:
- if overflow == "ellipsis" and no_wrap:
- line = line.truncate(width, ellipsis=True)
- content_line = _FormattedLine(line, width, y=y, align=align)
- new_lines = [content_line]
+ if no_wrap:
+ if overflow == "fold":
+ cuts = list(range(0, line.cell_length, width))[1:]
+ new_lines = [
+ _FormattedLine(line, width, y=y, align=align)
+ for line in line.divide(cuts)
+ ]
+ else:
+ line = line.truncate(width, ellipsis=overflow == "ellipsis")
+ content_line = _FormattedLine(line, width, y=y, align=align)
+ new_lines = [content_line]
else:
content_line = _FormattedLine(line, width, y=y, align=align)
offsets = divide_line(line.plain, width, fold=overflow == "fold")
@@ -495,6 +495,7 @@ def render_strips(
Returns:
An list of Strips.
"""
+
if not width:
return []
@@ -948,7 +949,7 @@ def render(
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 +972,13 @@ def render(
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:
diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py
index a715162f32..de1e98e84e 100644
--- a/src/textual/css/stylesheet.py
+++ b/src/textual/css/stylesheet.py
@@ -220,7 +220,7 @@ def set_variables(self, variables: dict[str, str]) -> None:
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 @@ def parse_style(self, style_text: str) -> Style:
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)
diff --git a/src/textual/fuzzy.py b/src/textual/fuzzy.py
index 337ad29b4d..9a7a55014d 100644
--- a/src/textual/fuzzy.py
+++ b/src/textual/fuzzy.py
@@ -12,8 +12,9 @@
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 @@ def match(self, candidate: str) -> float:
"""
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 @@ def highlight(self, candidate: str) -> Text:
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
diff --git a/src/textual/screen.py b/src/textual/screen.py
index a4406be081..edc58fa7fc 100644
--- a/src/textual/screen.py
+++ b/src/textual/screen.py
@@ -962,9 +962,7 @@ def _update_focus_styles(
widgets.update(widget.walk_children(with_self=True))
break
if widgets:
- self.app.stylesheet.update_nodes(
- [widget for widget in widgets if widget._has_focus_within], animate=True
- )
+ self.app.stylesheet.update_nodes(widgets, animate=True)
def set_focus(
self,
diff --git a/src/textual/strip.py b/src/textual/strip.py
index 2b2697a17d..dc69561a5b 100644
--- a/src/textual/strip.py
+++ b/src/textual/strip.py
@@ -8,7 +8,7 @@
from __future__ import annotations
from itertools import chain
-from typing import Iterable, Iterator, Sequence
+from typing import Any, Iterable, Iterator, Sequence
import rich.repr
from rich.cells import cell_len, set_cell_size
@@ -599,6 +599,19 @@ def apply_style(self, style: Style) -> Strip:
self._style_cache[style] = styled_strip
return styled_strip
+ def apply_meta(self, meta: dict[str, Any]) -> Strip:
+ """Apply meta to all segments.
+
+ Args:
+ meta: A dict of meta information.
+
+ Returns:
+ A new strip.
+
+ """
+ meta_style = Style.from_meta(meta)
+ return self.apply_style(meta_style)
+
def _apply_link_style(self, link_style: Style) -> Strip:
segments = self._segments
_Segment = Segment
diff --git a/src/textual/style.py b/src/textual/style.py
index 7403a283e8..c96c9b7af6 100644
--- a/src/textual/style.py
+++ b/src/textual/style.py
@@ -177,10 +177,14 @@ def __add__(self, other: object | None) -> 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 @@ def without_color(self) -> Style:
bold=self.bold,
dim=self.dim,
italic=self.italic,
+ underline=self.underline,
reverse=self.reverse,
strike=self.strike,
link=self.link,
diff --git a/src/textual/types.py b/src/textual/types.py
index c48653e529..6f9b16c847 100644
--- a/src/textual/types.py
+++ b/src/textual/types.py
@@ -20,7 +20,6 @@
from textual.widgets._input import InputValidationOn
from textual.widgets._option_list import (
DuplicateID,
- NewOptionListContent,
OptionDoesNotExist,
OptionListContent,
)
@@ -41,7 +40,6 @@
"IgnoreReturnCallbackType",
"InputValidationOn",
"MessageTarget",
- "NewOptionListContent",
"NoActiveAppError",
"NoSelection",
"OptionDoesNotExist",
diff --git a/src/textual/widget.py b/src/textual/widget.py
index cd64bd1976..a7a790a420 100644
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -1044,20 +1044,21 @@ def get_component_rich_style(self, *names: str, partial: bool = False) -> 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.
Args:
component_classes: Optional component styles.
+ partial: Return a partial style (not combined with parent).
Returns:
A Visual style instance.
"""
- if (
- visual_style := self._visual_style_cache.get(component_classes, None)
- ) is None:
- # TODO: cache this?
+ 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)
@@ -1066,8 +1067,11 @@ def get_visual_style(self, *component_classes: str) -> VisualStyle:
def iter_styles() -> Iterable[StylesBase]:
"""Iterate over the styles from the DOM and additional components styles."""
- for node in reversed(self.ancestors_with_self):
- yield node.styles
+ if partial:
+ node = self
+ else:
+ for node in reversed(self.ancestors_with_self):
+ yield node.styles
for name in component_classes:
yield node.get_component_styles(name)
@@ -1098,7 +1102,7 @@ def iter_styles() -> Iterable[StylesBase]:
underline=style.underline,
strike=style.strike,
)
- self._visual_style_cache[component_classes] = visual_style
+ self._visual_style_cache[cache_key] = visual_style
return visual_style
@@ -2406,7 +2410,6 @@ def _scroll_to(
Returns:
`True` if the scroll position changed, otherwise `False`.
"""
-
maybe_scroll_x = x is not None and (self.allow_horizontal_scroll or force)
maybe_scroll_y = y is not None and (self.allow_vertical_scroll or force)
scrolled_x = scrolled_y = False
@@ -3196,6 +3199,9 @@ def scroll_to_widget(
region = widget.virtual_region_with_margin
scrolled = False
+ if not region.size:
+ return False
+
while isinstance(widget.parent, Widget) and widget is not self:
container = widget.parent
if widget.styles.dock != "none":
@@ -4223,7 +4229,7 @@ def release_mouse(self) -> None:
"""
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)
@@ -4288,9 +4294,9 @@ async def _on_mouse_up(self, event: events.MouseUp) -> None:
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)
diff --git a/src/textual/widgets/_option_list.py b/src/textual/widgets/_option_list.py
index 65758f938b..878e6f57f5 100644
--- a/src/textual/widgets/_option_list.py
+++ b/src/textual/widgets/_option_list.py
@@ -1,123 +1,112 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, ClassVar, Iterable, NamedTuple
+from dataclasses import dataclass, field
+from typing import TYPE_CHECKING, ClassVar, Iterable, Sequence, cast
import rich.repr
-from rich.console import RenderableType
-from rich.measure import Measurement
-from rich.rule import Rule
-from rich.style import Style
+from rich.segment import Segment
from textual import _widget_navigation, events
-from textual._widget_navigation import Direction
+from textual._loop import loop_last
from textual.binding import Binding, BindingType
from textual.cache import LRUCache
-from textual.geometry import Region, Size
+from textual.css.styles import RulesMap
+from textual.geometry import Region, Size, clamp
from textual.message import Message
from textual.reactive import reactive
from textual.scroll_view import ScrollView
from textual.strip import Strip
-from textual.visual import Padding, Visual, visualize
+from textual.style import Style
+from textual.visual import Padding, Visual, VisualType, visualize
if TYPE_CHECKING:
from typing_extensions import Self, TypeAlias
- from textual.app import RenderResult
+OptionListContent: TypeAlias = "Option | VisualType | None"
+"""Types accepted in OptionList constructor and [add_options()][textual.widgets.OptionList.ads_options]."""
-class DuplicateID(Exception):
- """Raised if a duplicate ID is used when adding options to an option list."""
+class OptionListError(Exception):
+ """An error occurred in the option list."""
-class OptionDoesNotExist(Exception):
- """Raised when a request has been made for an option that doesn't exist."""
+class DuplicateID(OptionListError):
+ """Raised if a duplicate ID is used when adding options to an option list."""
-class Separator:
- """Class used to add a separator to an [OptionList][textual.widgets.OptionList]."""
+
+class OptionDoesNotExist(OptionListError):
+ """Raised when a request has been made for an option that doesn't exist."""
@rich.repr.auto
class Option:
- """Class that holds the details of an individual option."""
+ """This class holds details of options in the list."""
def __init__(
- self, prompt: RenderResult, id: str | None = None, disabled: bool = False
+ self, prompt: VisualType, id: str | None = None, disabled: bool = False
) -> None:
"""Initialise the option.
Args:
- prompt: The prompt for the option.
- id: The optional ID for the option.
- disabled: The initial enabled/disabled state. Enabled by default.
+ prompt: The prompt (text displayed) for the option.
+ id: An option ID for the option.
+ disabled: Disable the option (will be shown grayed out, and will not be selectable).
+
"""
self._prompt = prompt
+ self._visual: Visual | None = None
self._id = id
self.disabled = disabled
+ self._divider = False
@property
- def prompt(self) -> RenderResult:
- """The prompt for the option."""
- return self._prompt
-
- def set_prompt(self, prompt: RenderResult) -> None:
- """Set the prompt for the option.
-
- Args:
- prompt: The new prompt for the option.
- """
- self._prompt = prompt
-
- def visualize(self) -> object:
+ def prompt(self) -> VisualType:
+ """The original prompt."""
return self._prompt
@property
def id(self) -> str | None:
- """The optional ID for the option."""
+ """Optional ID for the option."""
return self._id
- def __rich_repr__(self) -> rich.repr.Result:
- yield "prompt", self.prompt
- yield "id", self.id, None
- yield "disabled", self.disabled, False
-
- def __rich__(self) -> RenderResult:
- return self._prompt
+ def _set_prompt(self, prompt: VisualType) -> None:
+ """Update the prompt.
+ Args:
+ prompt: New prompt.
-class OptionLineSpan(NamedTuple):
- """Class that holds the line span information for an option.
-
- An [Option][textual.widgets.option_list.Option] can have a prompt that
- spans multiple lines. Also, there's no requirement that every option in
- an option list has the same span information. So this structure is used
- to track the line that an option starts on, and how many lines it
- contains.
- """
+ """
+ self._prompt = prompt
+ self._visual = None
- first: int
- """The line position for the start of the option.."""
- line_count: int
- """The count of lines that make up the option."""
+ def __hash__(self) -> int:
+ return id(self)
+ def __rich_repr__(self) -> rich.repr.Result:
+ yield self._prompt
+ yield "id", self._id, None
+ yield "disabled", self.disabled, False
+ yield "_divider", self._divider, False
-OptionListContent: TypeAlias = "Option | Separator"
-"""The type of an item of content in the option list.
-This type represents all of the types that will be found in the list of
-content of the option list after it has been processed for addition.
-"""
+@dataclass
+class _LineCache:
+ """Cached line information."""
-NewOptionListContent: TypeAlias = "OptionListContent | None | RenderableType"
-"""The type of a new item of option list content to be added to an option list.
+ lines: list[tuple[int, int]] = field(default_factory=list)
+ heights: dict[int, int] = field(default_factory=dict)
+ index_to_line: dict[int, int] = field(default_factory=dict)
-This type represents all of the types that will be accepted when adding new
-content to the option list. This is a superset of [`OptionListContent`][textual.types.OptionListContent].
-"""
+ def clear(self) -> None:
+ """Reset all caches."""
+ self.lines.clear()
+ self.heights.clear()
+ self.index_to_line.clear()
class OptionList(ScrollView, can_focus=True):
- """A vertical option list with bounce-bar highlighting."""
+ """A navigable list of options."""
ALLOW_SELECT = False
BINDINGS: ClassVar[list[BindingType]] = [
@@ -176,7 +165,7 @@ class OptionList(ScrollView, can_focus=True):
}
& > .option-list--option-hover {
background: $block-hover-background;
- }
+ }
}
"""
@@ -200,10 +189,13 @@ 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."""
+ _mouse_hovering_over: reactive[int | None] = reactive(None)
+ """The index of the option under the mouse or `None`."""
+
class OptionMessage(Message):
"""Base class for all option messages."""
- def __init__(self, option_list: OptionList, index: int) -> None:
+ def __init__(self, option_list: OptionList, option: Option, index: int) -> None:
"""Initialise the option message.
Args:
@@ -213,9 +205,9 @@ def __init__(self, option_list: OptionList, index: int) -> None:
super().__init__()
self.option_list: OptionList = option_list
"""The option list that sent the message."""
- self.option: Option = option_list.get_option_at_index(index)
+ self.option: Option = option
"""The highlighted option."""
- self.option_id: str | None = self.option.id
+ self.option_id: str | None = option.id
"""The ID of the option that the message relates to."""
self.option_index: int = index
"""The index of the option that the message relates to."""
@@ -230,10 +222,13 @@ def control(self) -> OptionList:
return self.option_list
def __rich_repr__(self) -> rich.repr.Result:
- yield "option_list", self.option_list
- yield "option", self.option
- yield "option_id", self.option_id
- yield "option_index", self.option_index
+ try:
+ yield "option_list", self.option_list
+ yield "option", self.option
+ yield "option_id", self.option_id
+ yield "option_index", self.option_index
+ except AttributeError:
+ return
class OptionHighlighted(OptionMessage):
"""Message sent when an option is highlighted.
@@ -251,360 +246,304 @@ class OptionSelected(OptionMessage):
def __init__(
self,
- *content: NewOptionListContent,
+ *content: OptionListContent,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
- wrap: bool = True,
- tooltip: RenderableType | None = None,
+ markup: bool = True,
):
- """Initialise the option list.
+ """Initialize an OptionList.
Args:
- *content: The content for the option list.
- name: The name of the option list.
- id: The ID of the option list in the DOM.
- classes: The CSS classes of the option list.
- disabled: Whether the option list is disabled or not.
- wrap: Should prompts be auto-wrapped?
- tooltip: Optional tooltip.
+ *content: Positional arguments become the options.
+ name: Name of the OptionList.
+ id: The ID of the OptionList in the DOM.
+ classes: Initial CSS classes.
+ disabled: Disable the widget?
+ markup: Strips should be rendered as Textual markup if `True`, or plain text if `False`.
"""
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
+ self._markup = markup
+ self._options: list[Option] = []
+ """List of options."""
+ self._id_to_option: dict[str, Option] = {}
+ """Maps an Options's ID on to the option itself."""
+ self._option_to_index: dict[Option, int] = {}
+ """Maps an Option to it's index in self._options."""
+
+ self._option_render_cache: LRUCache[tuple[Option, Style], list[Strip]]
+ self._option_render_cache = LRUCache(maxsize=1024 * 2)
+ """Caches rendered options."""
+
+ self._line_cache = _LineCache()
+ """Used to cache additional information that can be recomputed."""
+
+ self.add_options(content)
+ if self._options:
+ # TODO: Inherited from previous version. Do we always want this?
+ self.action_first()
- self._wrap = wrap
- """Should we auto-wrap options?
-
- If `False` options wider than the list will be truncated.
- """
-
- self._contents: list[OptionListContent] = [
- self._make_content(item) for item in content
- ]
- """A list of the content of the option list.
-
- This is *every* item that makes up the content of the option list;
- this includes both the options *and* the separators (and any other
- decoration we could end up adding -- although I don't anticipate
- anything else at the moment; but padding around separators could be
- a thing, perhaps).
- """
+ @property
+ def options(self) -> Sequence[Option]:
+ """Sequence of options in the OptionList.
- self._options: list[Option] = [
- content for content in self._contents if isinstance(content, Option)
- ]
- """A list of the options within the option list.
+ !!! note "This is read-only"
- This is a list of references to just the options alone, ignoring the
- separators and potentially any other line-oriented option list
- content that isn't an option.
"""
+ return self._options
- self._option_ids: dict[str, int] = {
- option.id: index
- for index, option in enumerate(self._options)
- if option.id is not None
- }
- """A dictionary of option IDs and the option indexes they relate to."""
-
- self._content_render_cache: LRUCache[tuple[int, str, int], list[Strip]]
- self._content_render_cache = LRUCache(1024)
-
- self._lines: list[tuple[int, int]] | None = None
- self._spans: list[OptionLineSpan] | None = None
-
- self._mouse_hovering_over: int | None = None
- """Used to track what the mouse is hovering over."""
-
- if tooltip is not None:
- self.tooltip = tooltip
-
- if self._options:
- self.action_first()
+ @property
+ def option_count(self) -> int:
+ """The number of options."""
+ return len(self._options)
- def _left_gutter_width(self) -> int:
- """Returns the size of any left gutter that should be taken into account.
+ def clear_options(self) -> Self:
+ """Clear the content of the option list.
Returns:
- The width of the left gutter.
+ The `OptionList` instance.
"""
- return 0
-
- def _on_mount(self):
- self._populate()
-
- def _refresh_lines(self) -> None:
- self._lines = None
- self._spans = None
- self._content_render_cache.clear()
- self._populate()
-
- def notify_style_update(self) -> None:
- self._content_render_cache.clear()
- super().notify_style_update()
-
- def _on_resize(self):
- self._refresh_lines()
+ 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
- def _add_lines(
- self, new_content: list[OptionListContent], width: int, option_index=0
- ) -> None:
- """Add new lines.
+ def add_options(self, new_options: Iterable[OptionListContent]) -> Self:
+ """Add new options.
Args:
- new_content: New content to add.
- width: Width to render content.
- option_index: Starting option index.
+ new_options: Content of new options.
"""
- assert self._lines is not None
- assert self._spans is not None
-
- for index, content in enumerate(new_content, len(self._lines)):
- if isinstance(content, Option):
- height = len(
- self._render_option_content(
- index, content, "", width - self._left_gutter_width()
- )
- )
- self._spans.append(OptionLineSpan(len(self._lines), height))
- self._lines.extend(
- (option_index, y_offset) for y_offset in range(height)
- )
- option_index += 1
+ 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."
+ )
+
+ new_options = list(new_options)
+ if not new_options:
+ return self
+ if new_options[0] is None:
+ # Handle the case where the first new option is None,
+ # which would update the previous option.
+ # This is sub-optimal, but hopefully not a common occurrence
+ self._clear_caches()
+ options = self._options
+ add_option = self._options.append
+
+ for prompt in new_options:
+ if isinstance(prompt, Option):
+ option = prompt
+ elif prompt is None:
+ if options:
+ options[-1]._divider = True
+ continue
else:
- self._lines.append(OptionLineSpan(-1, 0))
+ option = Option(prompt)
+ self._option_to_index[option] = len(options)
+ if option._id is not None:
+ if option._id in self._id_to_option:
+ raise DuplicateID(f"Unable to add {option!r} due to duplicate ID")
+ self._id_to_option[option._id] = option
+ add_option(option)
+ if self.is_mounted:
+ self._update_lines()
+ return self
- self.virtual_size = Size(width, len(self._lines))
- self.refresh(layout=self.styles.auto_dimensions)
- self._scroll_update(self.virtual_size)
+ def add_option(self, option: Option | VisualType | None = None) -> Self:
+ """Add a new option to the end of the option list.
- def _populate(self) -> None:
- """Populate the lines data-structure."""
+ Args:
+ option: New option to add, or `None` for a separator.
- self._lines = []
- self._spans = []
+ Returns:
+ The `OptionList` instance.
- self._add_lines(
- self._contents,
- self.scrollable_content_region.width - self._left_gutter_width(),
- )
+ Raises:
+ DuplicateID: If there is an attempt to use a duplicate ID.
+ """
+ self.add_options([option])
+ return self
- def get_content_width(self, container: Size, viewport: Size) -> int:
- """Get maximum width of options."""
- console = self.app.console
- options = console.options
- padding = self.get_component_styles("option-list--option").padding
- padding_width = padding.width
- return (
- max(
- Measurement.get(console, options, option.prompt).maximum
- for option in self._options
- )
- + padding_width
- )
+ def get_option(self, option_id: str) -> Option:
+ """Get the option with the given ID.
- def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
- # Get the content height without requiring a refresh
- # TODO: Internal data structure could be simplified
- _render_option_content = self._render_option_content
- heights = [
- len(_render_option_content(index, option, "", width))
- for index, option in enumerate(self._options)
- ]
- separator_count = sum(
- 1 for content in self._contents if isinstance(content, Separator)
- )
- return sum(heights) + separator_count
+ Args:
+ option_id: The ID of the option to get.
- def _on_mouse_move(self, event: events.MouseMove) -> None:
- """React to the mouse moving.
+ Returns:
+ The option with the ID.
- Args:
- event: The mouse movement event.
+ Raises:
+ OptionDoesNotExist: If no option has the given ID.
"""
- self._mouse_hovering_over = event.style.meta.get("option")
- self.refresh()
-
- def _on_leave(self, _: events.Leave) -> None:
- """React to the mouse leaving the widget."""
- self._mouse_hovering_over = None
+ try:
+ return self._id_to_option[option_id]
+ except KeyError:
+ raise OptionDoesNotExist(
+ f"There is no option with an ID of {option_id!r}"
+ ) from None
- async def _on_click(self, event: events.Click) -> None:
- """React to the mouse being clicked on an item.
+ def get_option_index(self, option_id: str) -> int:
+ """Get the index (offset in `self.options`) of the option with the given ID.
Args:
- event: The click event.
+ option_id: The ID of the option to get the index of.
+
+ Returns:
+ The index of the item with the given ID.
+
+ Raises:
+ OptionDoesNotExist: If no option has the given ID.
"""
- 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
- ):
- self.highlighted = clicked_option
- self.action_select()
+ option = self.get_option(option_id)
+ return self._option_to_index[option]
- def _make_content(self, content: NewOptionListContent) -> OptionListContent:
- """Convert a single item of content for the list into a content type.
+ def get_option_at_index(self, index: int) -> Option:
+ """Get the option at the given index.
Args:
- content: The content to turn into a full option list type.
+ index: The index of the option to get.
Returns:
- The content, usable in the option list.
+ The option at that index.
+
+ Raises:
+ OptionDoesNotExist: If there is no option with the given index.
"""
- if isinstance(content, (Option, Separator)):
- return content
- if content is None:
- return Separator()
- return Option(content)
+ try:
+ return self._options[index]
+ except IndexError:
+ raise OptionDoesNotExist(
+ f"There is no option with an index of {index}"
+ ) from None
- def _render_option_content(
- self, option_index: int, content: RenderResult, component_class: str, width: int
- ) -> list[Strip]:
- """Render content for option and style.
+ def _set_option_disabled(self, index: int, disabled: bool) -> Self:
+ """Set the disabled state of an option in the list.
Args:
- option_index: Option index to render.
- content: Render result for prompt.
- component class: Additional component class.
- width: Desired width of render.
+ index: The index of the option to set the disabled state of.
+ disabled: The disabled state to set.
Returns:
- A list of strips.
+ The `OptionList` instance.
"""
- cache_key = (option_index, component_class, width)
- if (strips := self._content_render_cache.get(cache_key, None)) is not None:
- return strips
-
- visual = visualize(self, content)
- padding = self.get_component_styles("option-list--option").padding
- if padding:
- visual = Padding(visual, padding)
-
- component_class_list = ["option-list--option"]
- if component_class:
- component_class_list.append(component_class)
+ self._options[index].disabled = disabled
+ if index == self.highlighted:
+ self.highlighted = _widget_navigation.find_next_enabled(
+ self._options, anchor=index, direction=1
+ )
+ # TODO: Refresh only if the affected option is visible.
+ self.refresh()
+ return self
- visual_style = self.get_visual_style(*component_class_list)
+ def enable_option_at_index(self, index: int) -> Self:
+ """Enable the option at the given index.
- strips = Visual.to_strips(self, visual, width, None, visual_style, pad=True)
- style_meta = Style.from_meta({"option": option_index})
- strips = [strip.apply_style(style_meta) for strip in strips]
+ Returns:
+ The `OptionList` instance.
- self._content_render_cache[cache_key] = strips
- return strips
+ Raises:
+ OptionDoesNotExist: If there is no option with the given index.
+ """
+ try:
+ return self._set_option_disabled(index, False)
+ except IndexError:
+ raise OptionDoesNotExist(
+ f"There is no option with an index of {index}"
+ ) from None
- def _duplicate_id_check(self, candidate_items: list[OptionListContent]) -> None:
- """Check the items to be added for any duplicates.
+ def disable_option_at_index(self, index: int) -> Self:
+ """Disable the option at the given index.
- Args:
- candidate_items: The items that are going be added.
+ Returns:
+ The `OptionList` instance.
Raises:
- DuplicateID: If there is an attempt to use a duplicate ID.
+ OptionDoesNotExist: If there is no option with the given index.
"""
- # We're only interested in options, and only those that have IDs.
- new_options = [
- item
- for item in candidate_items
- if isinstance(item, Option) and item.id is not None
- ]
- # Get the set of new IDs that we're being given.
- new_option_ids = {option.id for option in new_options}
- # Now check for duplicates, both internally amongst the new items
- # incoming, and also against all the current known IDs.
- if len(new_options) != len(new_option_ids) or not new_option_ids.isdisjoint(
- self._option_ids
- ):
- raise DuplicateID("Attempt made to add options with duplicate IDs.")
-
- def add_options(self, items: Iterable[NewOptionListContent]) -> Self:
- """Add new options to the end of the option list.
+ try:
+ return self._set_option_disabled(index, True)
+ except IndexError:
+ raise OptionDoesNotExist(
+ f"There is no option with an index of {index}"
+ ) from None
+
+ def enable_option(self, option_id: str) -> Self:
+ """Enable the option with the given ID.
Args:
- items: The new items to add.
+ option_id: The ID of the option to enable.
Returns:
The `OptionList` instance.
Raises:
- DuplicateID: If there is an attempt to use a duplicate ID.
-
- Note:
- All options are checked for duplicate IDs *before* any option is
- added. A duplicate ID will cause none of the passed items to be
- added to the option list.
+ OptionDoesNotExist: If no option has the given ID.
"""
- # Only work if we have items to add; but don't make a fuss out of
- # zero items to add, just carry on like nothing happened.
- if self._lines is None:
- self._lines = []
- if self._spans is None:
- self._spans = []
- new_items = list(items)
- if new_items:
- option_index = len(self._options)
- # Turn any incoming values into valid content for the list.
- content = [self._make_content(item) for item in new_items]
- self._duplicate_id_check(content)
- self._contents.extend(content)
- # Pull out the content that is genuine options, create any new
- # ID mappings required, then add the new options to the option
- # list.
- new_options = [item for item in content if isinstance(item, Option)]
- for new_option_index, new_option in enumerate(
- new_options, start=len(self._options)
- ):
- if new_option.id:
- self._option_ids[new_option.id] = new_option_index
- self._options.extend(new_options)
-
- self._add_lines(
- content,
- self.scrollable_content_region.width - self._left_gutter_width(),
- option_index=option_index,
- )
- self.refresh(layout=True)
- return self
+ return self.enable_option_at_index(self.get_option_index(option_id))
- def add_option(self, item: NewOptionListContent = None) -> Self:
- """Add a new option to the end of the option list.
+ def disable_option(self, option_id: str) -> Self:
+ """Disable the option with the given ID.
Args:
- item: The new item to add.
+ option_id: The ID of the option to disable.
Returns:
The `OptionList` instance.
Raises:
- DuplicateID: If there is an attempt to use a duplicate ID.
+ OptionDoesNotExist: If no option has the given ID.
"""
- return self.add_options([item])
+ return self.disable_option_at_index(self.get_option_index(option_id))
- def _remove_option(self, index: int) -> None:
- """Remove an option from the option list.
+ def _remove_option(self, option: Option) -> Self:
+ """Remove the option with the given ID.
Args:
- index: The index of the item to remove.
+ option: The Option to return.
+
+ Returns:
+ The `OptionList` instance.
Raises:
- IndexError: If there is no option of the given index.
+ OptionDoesNotExist: If no option has the given ID.
"""
+
+ 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
+
option = self._options[index]
del self._options[index]
- del self._contents[self._contents.index(option)]
- # Decrement index of options after the one we just removed.
- self._option_ids = {
- option_id: option_index - 1 if option_index > index else option_index
- for option_id, option_index in self._option_ids.items()
- if option_index != index
- }
- self._refresh_lines()
- # Force a re-validation of the highlight.
+ if option._id is not None:
+ del self._id_to_option[option._id]
+ del self._option_to_index[option]
self.highlighted = self.highlighted
- self._mouse_hovering_over = None
+ 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.
@@ -618,8 +557,8 @@ def remove_option(self, option_id: str) -> Self:
Raises:
OptionDoesNotExist: If no option has the given ID.
"""
- self._remove_option(self.get_option_index(option_id))
- return self
+ option = self.get_option(option_id)
+ return self._remove_option(option)
def remove_option_at_index(self, index: int) -> Self:
"""Remove the option at the given index.
@@ -634,14 +573,14 @@ def remove_option_at_index(self, index: int) -> Self:
OptionDoesNotExist: If there is no option with the given index.
"""
try:
- self._remove_option(index)
+ option = self._options[index]
except IndexError:
raise OptionDoesNotExist(
- f"There is no option with an index of {index!r}"
+ f"Unable to remove; there is no option at index {index}"
) from None
- return self
+ return self._remove_option(option)
- def _replace_option_prompt(self, index: int, prompt: RenderableType) -> None:
+ def _replace_option_prompt(self, index: int, prompt: VisualType) -> None:
"""Replace the prompt of an option in the list.
Args:
@@ -651,10 +590,10 @@ def _replace_option_prompt(self, index: int, prompt: RenderableType) -> None:
Raises:
OptionDoesNotExist: If there is no option with the given index.
"""
- self.get_option_at_index(index).set_prompt(prompt)
- self._refresh_lines()
+ self.get_option_at_index(index)._set_prompt(prompt)
+ self._clear_caches()
- def replace_option_prompt(self, option_id: str, prompt: RenderableType) -> Self:
+ def replace_option_prompt(self, option_id: str, prompt: VisualType) -> Self:
"""Replace the prompt of the option with the given ID.
Args:
@@ -670,9 +609,7 @@ def replace_option_prompt(self, option_id: str, prompt: RenderableType) -> Self:
self._replace_option_prompt(self.get_option_index(option_id), prompt)
return self
- def replace_option_prompt_at_index(
- self, index: int, prompt: RenderableType
- ) -> Self:
+ def replace_option_prompt_at_index(self, index: int, prompt: VisualType) -> Self:
"""Replace the prompt of the option at the given index.
Args:
@@ -688,227 +625,274 @@ def replace_option_prompt_at_index(
self._replace_option_prompt(index, prompt)
return self
- def clear_options(self) -> Self:
- """Clear the content of the option list.
+ @property
+ def _lines(self) -> Sequence[tuple[int, int]]:
+ """A sequence of pairs of ints for each line, used internally.
+
+ The first int is the index of the option, and second is the line offset.
+
+ !!! note "This is read-only"
Returns:
- The `OptionList` instance.
+ A sequence of tuples.
"""
- self._contents.clear()
- self._options.clear()
- self._option_ids.clear()
- self.highlighted = None
- self._mouse_hovering_over = None
- self._refresh_lines()
- return self
+ self._update_lines()
+ return self._line_cache.lines
- def _set_option_disabled(self, index: int, disabled: bool) -> Self:
- """Set the disabled state of an option in the list.
+ @property
+ def _heights(self) -> dict[int, int]:
+ self._update_lines()
+ return self._line_cache.heights
- Args:
- index: The index of the option to set the disabled state of.
- disabled: The disabled state to set.
+ @property
+ def _index_to_line(self) -> dict[int, int]:
+ self._update_lines()
+ return self._line_cache.index_to_line
- Returns:
- The `OptionList` instance.
- """
- self._options[index].disabled = disabled
- if index == self.highlighted:
- self.highlighted = _widget_navigation.find_next_enabled(
- self._options, anchor=index, direction=1
- )
- # TODO: Refresh only if the affected option is visible.
+ def _clear_caches(self) -> None:
+ self._option_render_cache.clear()
+ self._line_cache.clear()
self.refresh()
- return self
- def enable_option_at_index(self, index: int) -> Self:
- """Enable the option at the given index.
+ def notify_style_update(self) -> None:
+ self._clear_caches()
+ super().notify_style_update()
- Returns:
- The `OptionList` instance.
+ def _on_resize(self):
+ self._clear_caches()
- Raises:
- OptionDoesNotExist: If there is no option with the given index.
- """
- try:
- return self._set_option_disabled(index, False)
- except IndexError:
- raise OptionDoesNotExist(
- f"There is no option with an index of {index}"
- ) from None
+ def on_show(self) -> None:
+ self.scroll_to_highlight()
- def disable_option_at_index(self, index: int) -> Self:
- """Disable the option at the given index.
+ def on_mount(self) -> None:
+ self._update_lines()
- Returns:
- The `OptionList` instance.
+ async def _on_click(self, event: events.Click) -> None:
+ """React to the mouse being clicked on an item.
- Raises:
- OptionDoesNotExist: If there is no option with the given index.
+ Args:
+ event: The click event.
"""
- try:
- return self._set_option_disabled(index, True)
- except IndexError:
- raise OptionDoesNotExist(
- f"There is no option with an index of {index}"
- ) from None
-
- def enable_option(self, option_id: str) -> Self:
- """Enable the option with the given ID.
+ clicked_option: int | None = event.style.meta.get("option")
+ if clicked_option is not None and not self._options[clicked_option].disabled:
+ self.highlighted = clicked_option
+ self.action_select()
- Args:
- option_id: The ID of the option to enable.
+ def _get_left_gutter_width(self) -> int:
+ """Returns the size of any left gutter that should be taken into account.
Returns:
- The `OptionList` instance.
-
- Raises:
- OptionDoesNotExist: If no option has the given ID.
+ The width of the left gutter.
"""
- return self.enable_option_at_index(self.get_option_index(option_id))
+ return 0
- def disable_option(self, option_id: str) -> Self:
- """Disable the option with the given ID.
+ def _on_mouse_move(self, event: events.MouseMove) -> None:
+ """React to the mouse moving.
Args:
- option_id: The ID of the option to disable.
-
- Returns:
- The `OptionList` instance.
-
- Raises:
- OptionDoesNotExist: If no option has the given ID.
+ event: The mouse movement event.
"""
- return self.disable_option_at_index(self.get_option_index(option_id))
+ self._mouse_hovering_over = event.style.meta.get("option")
- @property
- def option_count(self) -> int:
- """The count of options."""
- return len(self._options)
+ def _on_leave(self, _: events.Leave) -> None:
+ """React to the mouse leaving the widget."""
+ self._mouse_hovering_over = None
- def get_option_at_index(self, index: int) -> Option:
- """Get the option at the given index.
+ def _get_visual(self, option: Option) -> Visual:
+ """Get a visual for the given option.
Args:
- index: The index of the option to get.
+ option: An option.
Returns:
- The option at that index.
+ A Visual.
- Raises:
- OptionDoesNotExist: If there is no option with the given index.
"""
- try:
- return self._options[index]
- except IndexError:
- raise OptionDoesNotExist(
- f"There is no option with an index of {index}"
- ) from None
+ if (visual := option._visual) is None:
+ visual = visualize(self, option.prompt, markup=self._markup)
+ option._visual = visual
+ return visual
- def get_option(self, option_id: str) -> Option:
- """Get the option with the given ID.
+ def _get_visual_from_index(self, index: int) -> Visual:
+ """Get a visual from the given index.
Args:
- option_id: The ID of the option to get.
+ index: An index (offset in self.options).
Returns:
- The option with the ID.
-
- Raises:
- OptionDoesNotExist: If no option has the given ID.
+ A Visual.
"""
- return self.get_option_at_index(self.get_option_index(option_id))
+ option = self.get_option_at_index(index)
+ return self._get_visual(option)
- def get_option_index(self, option_id: str) -> int:
- """Get the index of the option with the given ID.
+ def _get_option_render(self, option: Option, style: Style) -> list[Strip]:
+ """Get rendered option with a given style.
Args:
- option_id: The ID of the option to get the index of.
+ style: Style of render.
+ index: Index of the option.
Returns:
- The index of the item with the given ID.
-
- Raises:
- OptionDoesNotExist: If no option has the given ID.
+ A list of strips.
"""
- try:
- return self._option_ids[option_id]
- except KeyError:
- raise OptionDoesNotExist(
- f"There is no option with an ID of '{option_id}'"
- ) from None
+ padding = self.get_component_styles("option-list--option").padding
+ render_width = self.scrollable_content_region.width
+ width = render_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)
+ if padding:
+ visual = Padding(visual, padding)
+ strips = visual.to_strips(self, visual, width, None, style)
+ meta = {"option": self._option_to_index[option]}
+ strips = [
+ strip.extend_cell_length(width, style.rich_style).apply_meta(meta)
+ for strip in strips
+ ]
+ if option._divider:
+ style = self.get_visual_style("option-list--separator")
+ rule_segments = [Segment("─" * width, style.rich_style)]
+ strips.append(Strip(rule_segments, width))
+ self._option_render_cache[cache_key] = strips
+ return strips
- def render_line(self, y: int) -> Strip:
- assert self._lines is not None
- if not self._lines:
- self._populate()
+ def _update_lines(self) -> None:
+ """Update internal structures when new lines are added."""
+ if not self.options or not self.scrollable_content_region:
+ # No options -- nothing to
+ return
- _scroll_x, scroll_y = self.scroll_offset
- line_number = scroll_y + y
+ line_cache = self._line_cache
+ lines = line_cache.lines
+ next_index = lines[-1][0] + 1 if lines else 0
+ get_visual = self._get_visual
+ width = self.scrollable_content_region.width - self._get_left_gutter_width()
+
+ if next_index < len(self.options):
+ padding = self.get_component_styles("option-list--option").padding
+ 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(self.styles, width - padding.width)
+ + option._divider
+ )
+ line_cache.heights[index] = line_count
+ line_cache.lines.extend(
+ [(index, line_no) for line_no in range(0, line_count)]
+ )
- try:
- option_index, y_offset = self._lines[line_number]
- except IndexError:
- return Strip([])
+ last_divider = self.options and self.options[-1]._divider
+ self.virtual_size = Size(width, len(lines) - (1 if last_divider else 0))
+ self._scroll_update(self.virtual_size)
- renderable = (
- Rule(style=self.get_component_rich_style("option-list--separator"))
- if option_index == -1
- else self._options[option_index]
+ def get_content_width(self, container: Size, viewport: Size) -> int:
+ """Get maximum width of options."""
+ if not self.options:
+ return 0
+ styles = self.styles
+ get_visual_from_index = self._get_visual_from_index
+ padding = self.get_component_styles("option-list--option").padding
+ gutter_width = self._get_left_gutter_width()
+ container_width = container.width
+ width = (
+ max(
+ get_visual_from_index(index).get_optimal_width(styles, container_width)
+ for index in range(len(self.options))
+ )
+ + padding.width
+ + gutter_width
)
+ return width
- mouse_over = self._mouse_hovering_over == option_index
+ def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
+ """Get height for the given width."""
+ styles = self.get_component_styles("option-list--option")
+ rules = cast(RulesMap, styles)
+ padding_width = styles.padding.width
+ get_visual = self._get_visual
+ height = sum(
+ (
+ get_visual(option).get_height(rules, width - padding_width)
+ + (1 if option._divider and not last else 0)
+ )
+ for last, option in loop_last(self.options)
+ )
+ return height
- component_class: str = ""
+ def _get_line(self, style: Style, y: int) -> Strip:
+ index, line_offset = self._lines[y]
+ option = self.get_option_at_index(index)
+ strips = self._get_option_render(option, style)
+ return strips[line_offset]
- if option_index == -1:
- component_class = "option-list--separator"
- else:
- try:
- option = self._options[option_index]
- except IndexError:
- pass
- else:
- if option.disabled:
- component_class = "option-list--option-disabled"
- elif self.highlighted == option_index:
- component_class = "option-list--option-highlighted"
- elif mouse_over:
- component_class = "option-list--option-hover"
-
- strips = self._render_option_content(
- option_index,
- renderable,
- component_class,
- self.scrollable_content_region.width - self._left_gutter_width(),
- )
+ 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:
- strip = strips[y_offset]
+ option_index, line_offset = self._lines[line_number]
except IndexError:
- return Strip([])
+ return Strip.blank(self.scrollable_content_region.width)
+
+ option = self.options[option_index]
+ mouse_over = self._mouse_hovering_over == option_index
+ component_class = ""
+ if option.disabled:
+ component_class = "option-list--option-disabled"
+ elif self.highlighted == option_index:
+ component_class = "option-list--option-highlighted"
+ elif mouse_over:
+ 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")
+
+ strips = self._get_option_render(option, style)
+ strip = strips[line_offset]
return strip
+ def validate_highlighted(self, highlighted: int | None) -> int | None:
+ """Validate the `highlighted` property value on access."""
+ if highlighted is None or not self.options:
+ return None
+ elif highlighted < 0:
+ return 0
+ elif highlighted >= len(self.options):
+ return len(self.options) - 1
+ return highlighted
+
+ def watch_highlighted(self, highlighted: int | None) -> None:
+ """React to the highlighted option having changed."""
+ if highlighted is None:
+ return
+ if not self._options[highlighted].disabled:
+ self.scroll_to_highlight()
+ self.post_message(
+ self.OptionHighlighted(self, self.options[highlighted], highlighted)
+ )
+
def scroll_to_highlight(self, top: bool = False) -> None:
- """Ensure that the highlighted option is in view.
+ """Scroll to the highlighted option.
Args:
- top: Scroll highlight to top of the list.
+ top: Ensure highlighted option is at the top of the widget.
"""
highlighted = self.highlighted
-
if highlighted is None or not self.is_mounted:
return
- if not self._spans:
- self._populate()
+ self._update_lines()
try:
- y, height = self._spans[highlighted]
- except IndexError:
- # Index error means we're being asked to scroll to a highlight
- # before all the tracking information has been worked out.
- # That's fine; let's just NoP that.
+ y = self._index_to_line[highlighted]
+ except KeyError:
return
+ height = self._heights[highlighted]
+
self.scroll_to_region(
Region(0, y, self.scrollable_content_region.width, height),
force=True,
@@ -917,31 +901,10 @@ def scroll_to_highlight(self, top: bool = False) -> None:
immediate=True,
)
- def on_show(self) -> None:
- if self.highlighted is not None:
- self.scroll_to_highlight()
-
- def validate_highlighted(self, highlighted: int | None) -> int | None:
- """Validate the `highlighted` property value on access."""
- if highlighted is None or not self._options:
- return None
- elif highlighted < 0:
- return 0
- elif highlighted >= len(self._options):
- return len(self._options) - 1
-
- return highlighted
-
- def watch_highlighted(self, highlighted: int | None) -> None:
- """React to the highlighted option having changed."""
- if highlighted is not None and not self._options[highlighted].disabled:
- self.scroll_to_highlight()
- self.post_message(self.OptionHighlighted(self, highlighted))
-
def action_cursor_up(self) -> None:
"""Move the highlight up to the previous enabled option."""
self.highlighted = _widget_navigation.find_next_enabled(
- self._options,
+ self.options,
anchor=self.highlighted,
direction=-1,
)
@@ -949,106 +912,67 @@ def action_cursor_up(self) -> None:
def action_cursor_down(self) -> None:
"""Move the highlight down to the next enabled option."""
self.highlighted = _widget_navigation.find_next_enabled(
- self._options,
+ self.options,
anchor=self.highlighted,
direction=1,
)
def action_first(self) -> None:
"""Move the highlight to the first enabled option."""
- self.highlighted = _widget_navigation.find_first_enabled(self._options)
+ self.highlighted = _widget_navigation.find_first_enabled(self.options)
def action_last(self) -> None:
"""Move the highlight to the last enabled option."""
- self.highlighted = _widget_navigation.find_last_enabled(self._options)
+ self.highlighted = _widget_navigation.find_last_enabled(self.options)
- def _page(self, direction: Direction) -> None:
- """Move the highlight roughly by one page in the given direction.
+ def _move_page(self, direction: _widget_navigation.Direction) -> None:
+ """Move the height roughly by one page in the given direction.
- The highlight will tentatively move by exactly one page.
- If this would result in highlighting a disabled option, instead we look for
- an enabled option "further down" the list of options.
- If there are no such enabled options, we fallback to the "last" enabled option.
- (The meaning of "further down" and "last" depend on the direction specified.)
+ This method will attempt to avoid selecting a disabled option.
Args:
- direction: The direction to head, -1 for up and 1 for down.
+ direction: `-1` to move up a page, `1` to move down a page.
"""
+ if not self._options:
+ return
- # If we find ourselves in a position where we don't know where we're
- # going, we need a fallback location. Where we go will depend on the
- # direction.
- assert self._spans is not None
- assert self._lines is not None
+ 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]
+ self.highlighted = _widget_navigation.find_next_enabled_no_wrap(
+ candidates=self._options,
+ anchor=option_index,
+ direction=direction,
+ with_anchor=True,
+ )
- fallback = self.action_first if direction == -1 else self.action_last
+ def action_page_up(self):
+ """Move the highlight up one page."""
+ if self.highlighted is None:
+ self.action_first()
+ else:
+ self._move_page(-1)
- highlighted = self.highlighted
- if highlighted is None:
- # There is no highlight yet so let's go to the default position.
- fallback()
+ def action_page_down(self):
+ """Move the highlight down one page."""
+ if self.highlighted is None:
+ self.action_last()
else:
- # We want to page roughly by lines, but we're dealing with
- # options that can be a varying number of lines in height. So
- # let's start with the target line alone.
- target_line = max(
- 0,
- self._spans[highlighted].first
- + (direction * self.scrollable_content_region.height),
- )
- try:
- # Now that we've got a target line, let's figure out the
- # index of the target option.
- target_option: int | None = self._lines[target_line][0]
- except IndexError:
- # An index error suggests we've gone out of bounds, let's
- # settle on whatever the call thinks is a good place to wrap
- # to.
- fallback()
- else:
- # Looks like we've figured where we'd like to jump to, we
- # just need to make sure we jump to an option that's enabled.
- if target_option is not None:
- target_option = _widget_navigation.find_next_enabled_no_wrap(
- candidates=self._options,
- anchor=target_option,
- direction=direction,
- with_anchor=True,
- )
- # If we couldn't find an enabled option that's at least one page
- # away from the current one, we instead move less than one page
- # to the last enabled option in the correct direction.
- if target_option is None:
- fallback()
- else:
- self.highlighted = target_option
-
- def action_page_up(self) -> None:
- """Move the highlight up roughly by one page."""
- self._page(-1)
-
- def action_page_down(self) -> None:
- """Move the highlight down roughly by one page."""
- self._page(1)
+ self._move_page(1)
def action_select(self) -> None:
- """Select the currently-highlighted option.
+ """Select the currently highlighted option.
- If no option is selected, then nothing happens. If an option is
- selected, a [OptionList.OptionSelected][textual.widgets.OptionList.OptionSelected]
- message will be posted.
+ If an option is selected then a
+ [OptionList.OptionSelected][textual.widgets.OptionList.OptionSelected] will be posted.
"""
highlighted = self.highlighted
- if highlighted is not None and not self._options[highlighted].disabled:
- self.post_message(self.OptionSelected(self, highlighted))
-
-
-if __name__ == "__main__":
- from textual.app import App, ComposeResult
-
- class OptionApp(App):
- def compose(self) -> ComposeResult:
- yield OptionList("Foo", "Bar", "Baz")
-
- app = OptionApp()
- app.run()
+ 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))
diff --git a/src/textual/widgets/_selection_list.py b/src/textual/widgets/_selection_list.py
index 1d147377cf..8ef829491c 100644
--- a/src/textual/widgets/_selection_list.py
+++ b/src/textual/widgets/_selection_list.py
@@ -15,7 +15,12 @@
from textual.binding import Binding
from textual.messages import Message
from textual.strip import Strip
-from textual.widgets._option_list import NewOptionListContent, Option, OptionList
+from textual.widgets._option_list import (
+ Option,
+ OptionDoesNotExist,
+ OptionList,
+ OptionListContent,
+)
from textual.widgets._toggle_button import ToggleButton
SelectionType = TypeVar("SelectionType")
@@ -229,18 +234,11 @@ def __init__(
self._send_messages = False
"""Keep track of when we're ready to start sending messages."""
options = [self._make_selection(selection) for selection in selections]
- super().__init__(
- *options,
- name=name,
- id=id,
- classes=classes,
- 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."""
+ super().__init__(*options, name=name, id=id, classes=classes, disabled=disabled)
@property
def selected(self) -> list[SelectionType]:
@@ -487,7 +485,7 @@ def _toggle_highlighted_selection(self) -> None:
if self.highlighted is not None:
self.toggle(self.get_option_at_index(self.highlighted))
- def _left_gutter_width(self) -> int:
+ def _get_left_gutter_width(self) -> int:
"""Returns the size of any left gutter that should be taken into account.
Returns:
@@ -510,20 +508,20 @@ def render_line(self, y: int) -> Strip:
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)
# 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
- selection = self.get_option_at_index(selection_index)
+ 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,20 +531,14 @@ def render_line(self, y: int) -> Strip:
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.
+ # TODO: This is not a reliable way of getting the base style
+ underlying_style = next(iter(line)).style or self.rich_style
assert underlying_style is not None
# Get the style for the button.
button_style = self.get_component_rich_style(component_style)
- # If the button is in the unselected state, we're going to do a bit
- # of a switcharound to make it look like it's a "cutout".
- # if selection.value not in self._selected:
- # button_style += Style.from_color(
- # self.background_colors[1].rich_color, button_style.bgcolor
- # )
-
# Build the style for the side characters. Note that this is
# sensitive to the type of character used, so pay attention to
# BUTTON_LEFT and BUTTON_RIGHT.
@@ -565,7 +557,7 @@ def render_line(self, y: int) -> Strip:
Segment(ToggleButton.BUTTON_INNER, style=button_style),
Segment(ToggleButton.BUTTON_RIGHT, style=side_style),
Segment(" ", style=underlying_style),
- *prompt,
+ *line,
]
)
@@ -617,29 +609,22 @@ def get_option(self, option_id: str) -> Selection[SelectionType]:
"""
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,
items: Iterable[
- NewOptionListContent
+ OptionListContent
| Selection[SelectionType]
| tuple[TextType, SelectionType]
| tuple[TextType, SelectionType, bool]
@@ -694,7 +679,7 @@ def add_options(
def add_option(
self,
item: (
- NewOptionListContent
+ OptionListContent
| Selection
| tuple[TextType, SelectionType]
| tuple[TextType, SelectionType, bool]
diff --git a/src/textual/widgets/option_list.py b/src/textual/widgets/option_list.py
index c24d83cc11..9e305cb395 100644
--- a/src/textual/widgets/option_list.py
+++ b/src/textual/widgets/option_list.py
@@ -1,8 +1,3 @@
-from textual.widgets._option_list import (
- DuplicateID,
- Option,
- OptionDoesNotExist,
- Separator,
-)
+from textual.widgets._option_list import DuplicateID, Option, OptionDoesNotExist
-__all__ = ["DuplicateID", "Option", "OptionDoesNotExist", "Separator"]
+__all__ = ["DuplicateID", "Option", "OptionDoesNotExist"]
diff --git a/tests/option_list/test_option_list_create.py b/tests/option_list/test_option_list_create.py
index 69ccce06dc..03c0c8772f 100644
--- a/tests/option_list/test_option_list_create.py
+++ b/tests/option_list/test_option_list_create.py
@@ -6,12 +6,7 @@
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 @@ def compose(self) -> ComposeResult:
yield OptionList(
"0",
Option("1"),
- Separator(),
+ None,
Option("2", disabled=True),
None,
Option("3", id="3"),
diff --git a/tests/option_list/test_option_list_mouse_click.py b/tests/option_list/test_option_list_mouse_click.py
index ec0182c470..e41d9c5289 100644
--- a/tests/option_list/test_option_list_mouse_click.py
+++ b/tests/option_list/test_option_list_mouse_click.py
@@ -3,7 +3,7 @@
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"),
)
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_add_separator.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_add_separator.svg
new file mode 100644
index 0000000000..9ec842e23b
--- /dev/null
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_add_separator.svg
@@ -0,0 +1,156 @@
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_click_expand.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_click_expand.svg
index 52efcdf6aa..085a42ce0d 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_click_expand.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_click_expand.svg
@@ -19,139 +19,139 @@
font-weight: 700;
}
- .terminal-2892528411-matrix {
+ .terminal-2024260676-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-2892528411-title {
+ .terminal-2024260676-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-2892528411-r1 { fill: #121212 }
-.terminal-2892528411-r2 { fill: #191919 }
-.terminal-2892528411-r3 { fill: #c5c8c6 }
-.terminal-2892528411-r4 { fill: #e0e0e0 }
-.terminal-2892528411-r5 { fill: #7f7f7f }
-.terminal-2892528411-r6 { fill: #0178d4 }
-.terminal-2892528411-r7 { fill: #003054 }
-.terminal-2892528411-r8 { fill: #272727 }
-.terminal-2892528411-r9 { fill: #000000 }
-.terminal-2892528411-r10 { fill: #ddedf9;font-weight: bold }
+ .terminal-2024260676-r1 { fill: #121212 }
+.terminal-2024260676-r2 { fill: #191919 }
+.terminal-2024260676-r3 { fill: #c5c8c6 }
+.terminal-2024260676-r4 { fill: #e0e0e0 }
+.terminal-2024260676-r5 { fill: #7f7f7f }
+.terminal-2024260676-r6 { fill: #0178d4 }
+.terminal-2024260676-r7 { fill: #003054 }
+.terminal-2024260676-r8 { fill: #272727 }
+.terminal-2024260676-r9 { fill: #000000 }
+.terminal-2024260676-r10 { fill: #ddedf9;font-weight: bold }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- SelectApp
+ SelectApp
-
-
-
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊15▲▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊ 6 ▎
-▊ 7 ▎
-▊ 8 ▎
-▊ 9 ▆▆▎
-▊ 10 ▎
-▊ 11 ▎
-▊ 12 ▎
-▊ 13 ▎
-▊ 14 ▇▇▎
-▊ 15 ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-
-
-
-
-
-
-
-
+
+
+
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊15▲▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊6▎
+▊7▎
+▊8▎
+▊9▆▆▎
+▊10▎
+▊11▎
+▊12▎
+▊13▎
+▊14▇▇▎
+▊15▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+
+
+
+
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_disabled.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_disabled.svg
index d6ad664bf7..765e14674c 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_disabled.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_disabled.svg
@@ -19,140 +19,140 @@
font-weight: 700;
}
- .terminal-349561797-matrix {
+ .terminal-1302620672-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-349561797-title {
+ .terminal-1302620672-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-349561797-r1 { fill: #e0e0e0 }
-.terminal-349561797-r2 { fill: #c5c8c6 }
-.terminal-349561797-r3 { fill: #a2a2a2 }
-.terminal-349561797-r4 { fill: #a5a5a5 }
-.terminal-349561797-r5 { fill: #a4a4a4 }
-.terminal-349561797-r6 { fill: #a2a2a2;font-weight: bold }
-.terminal-349561797-r7 { fill: #121212 }
-.terminal-349561797-r8 { fill: #141414 }
-.terminal-349561797-r9 { fill: #1a1a1a }
-.terminal-349561797-r10 { fill: #1c2126 }
-.terminal-349561797-r11 { fill: #050f16 }
+ .terminal-1302620672-r1 { fill: #e0e0e0 }
+.terminal-1302620672-r2 { fill: #c5c8c6 }
+.terminal-1302620672-r3 { fill: #a2a2a2 }
+.terminal-1302620672-r4 { fill: #a5a5a5 }
+.terminal-1302620672-r5 { fill: #a4a4a4 }
+.terminal-1302620672-r6 { fill: #a2a2a2;font-weight: bold }
+.terminal-1302620672-r7 { fill: #121212 }
+.terminal-1302620672-r8 { fill: #141414 }
+.terminal-1302620672-r9 { fill: #1a1a1a }
+.terminal-1302620672-r10 { fill: #1c2126 }
+.terminal-1302620672-r11 { fill: #050f16 }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- DisabledApp
+ DisabledApp
-
-
-
- Labels don't have a disabled state
-I am disabled
-
-
-
-I am disabled
-
-
-
- Foo Bar
- Also disabled
-
-
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊you ▎
-▊can't ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊▐X▌ Simple SelectionList ▎
-▊▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-
-
+
+
+
+ Labels don't have a disabled state
+I am disabled
+
+
+
+I am disabled
+
+
+
+ Foo Bar
+ Also disabled
+
+
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊you▎
+▊can't▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊▐X▌ Simple SelectionList ▎
+▊▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_empty_option_list.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_empty_option_list.svg
new file mode 100644
index 0000000000..3d4fdd276a
--- /dev/null
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_empty_option_list.svg
@@ -0,0 +1,152 @@
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_focus_within_transparent.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_focus_within_transparent.svg
new file mode 100644
index 0000000000..4a87577766
--- /dev/null
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_focus_within_transparent.svg
@@ -0,0 +1,154 @@
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_loading_indicator.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_loading_indicator.svg
index 09d2bddfe1..2a52e2d4c7 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_loading_indicator.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_loading_indicator.svg
@@ -19,138 +19,138 @@
font-weight: 700;
}
- .terminal-3728132459-matrix {
+ .terminal-64002133-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-3728132459-title {
+ .terminal-64002133-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-3728132459-r1 { fill: #121212 }
-.terminal-3728132459-r2 { fill: #0178d4 }
-.terminal-3728132459-r3 { fill: #191919 }
-.terminal-3728132459-r4 { fill: #c5c8c6 }
-.terminal-3728132459-r5 { fill: #004578 }
-.terminal-3728132459-r6 { fill: #e0e0e0 }
-.terminal-3728132459-r7 { fill: #1e1e1e }
-.terminal-3728132459-r8 { fill: #000000 }
+ .terminal-64002133-r1 { fill: #121212 }
+.terminal-64002133-r2 { fill: #0178d4 }
+.terminal-64002133-r3 { fill: #191919 }
+.terminal-64002133-r4 { fill: #c5c8c6 }
+.terminal-64002133-r5 { fill: #004578 }
+.terminal-64002133-r6 { fill: #e0e0e0 }
+.terminal-64002133-r7 { fill: #1e1e1e }
+.terminal-64002133-r8 { fill: #000000 }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- LoadingOverlayRedux
+ LoadingOverlayRedux
-
-
-
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊▎▊foo barfoo barfoo barfoo barfoo ▎
-▊▎▊bar ▎
-▊▎▊foo barfoo barfoo barfoo barfoo ▄▄▎
-▊▎▊bar ▎
-▊▎▊foo barfoo barfoo barfoo barfoo ▎
-▊▎▊bar ▎
-▊▎▊foo barfoo barfoo barfoo barfoo ▎
-▊▎▊bar ▎
-▊▎▊foo barfoo barfoo barfoo barfoo ▎
-▊▎▊bar ▎
-▊Loading!▎▊foo barfoo barfoo barfoo barfoo ▎
-▊▎▊bar ▎
-▊▎▊foo barfoo barfoo barfoo barfoo ▎
-▊▎▊bar ▎
-▊▎▊foo barfoo barfoo barfoo barfoo ▎
-▊▎▊bar ▎
-▊▎▊foo barfoo barfoo barfoo barfoo ▎
-▊▎▊bar ▎
-▊▎▊foo barfoo barfoo barfoo barfoo ▎
-▊▎▊bar ▎
-▊▎▊foo barfoo barfoo barfoo barfoo ▎
-▊▎▊bar ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊▎▊foo barfoo barfoo barfoo barfoo ▎
+▊▎▊bar▎
+▊▎▊foo barfoo barfoo barfoo barfoo ▄▄▎
+▊▎▊bar▎
+▊▎▊foo barfoo barfoo barfoo barfoo ▎
+▊▎▊bar▎
+▊▎▊foo barfoo barfoo barfoo barfoo ▎
+▊▎▊bar▎
+▊▎▊foo barfoo barfoo barfoo barfoo ▎
+▊▎▊bar▎
+▊Loading!▎▊foo barfoo barfoo barfoo barfoo ▎
+▊▎▊bar▎
+▊▎▊foo barfoo barfoo barfoo barfoo ▎
+▊▎▊bar▎
+▊▎▊foo barfoo barfoo barfoo barfoo ▎
+▊▎▊bar▎
+▊▎▊foo barfoo barfoo barfoo barfoo ▎
+▊▎▊bar▎
+▊▎▊foo barfoo barfoo barfoo barfoo ▎
+▊▎▊bar▎
+▊▎▊foo barfoo barfoo barfoo barfoo ▎
+▊▎▊bar▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_loading_indicator_disables_widget.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_loading_indicator_disables_widget.svg
index ad1e2cfb17..e12b2d666c 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_loading_indicator_disables_widget.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_loading_indicator_disables_widget.svg
@@ -19,138 +19,138 @@
font-weight: 700;
}
- .terminal-209568394-matrix {
+ .terminal-1906336014-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-209568394-title {
+ .terminal-1906336014-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-209568394-r1 { fill: #121212 }
-.terminal-209568394-r2 { fill: #0178d4 }
-.terminal-209568394-r3 { fill: #191919 }
-.terminal-209568394-r4 { fill: #c5c8c6 }
-.terminal-209568394-r5 { fill: #ddedf9;font-weight: bold }
-.terminal-209568394-r6 { fill: #1e1e1e }
-.terminal-209568394-r7 { fill: #e0e0e0 }
-.terminal-209568394-r8 { fill: #000000 }
+ .terminal-1906336014-r1 { fill: #121212 }
+.terminal-1906336014-r2 { fill: #0178d4 }
+.terminal-1906336014-r3 { fill: #191919 }
+.terminal-1906336014-r4 { fill: #c5c8c6 }
+.terminal-1906336014-r5 { fill: #ddedf9;font-weight: bold }
+.terminal-1906336014-r6 { fill: #1e1e1e }
+.terminal-1906336014-r7 { fill: #e0e0e0 }
+.terminal-1906336014-r8 { fill: #000000 }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- LoadingOverlayRedux
+ LoadingOverlayRedux
-
-
-
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
-▊world hello world hello world ▎▊bar ▎
-▊hello world hello world hello ▄▄▎▊foo barfoo barfoo barfoo barfoo ▄▄▎
-▊world hello world hello world ▎▊bar ▎
-▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
-▊world hello world hello world ▎▊bar ▎
-▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
-▊world hello world hello world ▎▊bar ▎
-▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
-▊world hello world hello world ▎▊bar ▎
-▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
-▊world hello world hello world ▎▊bar ▎
-▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
-▊world hello world hello world ▎▊bar ▎
-▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
-▊world hello world hello world ▎▊bar ▎
-▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
-▊world hello world hello world ▎▊bar ▎
-▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
-▊world hello world hello world ▎▊bar ▎
-▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
-▊world hello world hello world ▎▊bar ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
+▊world hello world hello world ▎▊bar▎
+▊hello world hello world hello ▄▄▎▊foo barfoo barfoo barfoo barfoo ▄▄▎
+▊world hello world hello world ▎▊bar▎
+▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
+▊world hello world hello world ▎▊bar▎
+▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
+▊world hello world hello world ▎▊bar▎
+▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
+▊world hello world hello world ▎▊bar▎
+▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
+▊world hello world hello world ▎▊bar▎
+▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
+▊world hello world hello world ▎▊bar▎
+▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
+▊world hello world hello world ▎▊bar▎
+▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
+▊world hello world hello world ▎▊bar▎
+▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
+▊world hello world hello world ▎▊bar▎
+▊hello world hello world hello ▎▊foo barfoo barfoo barfoo barfoo ▎
+▊world hello world hello world ▎▊bar▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_missing_vertical_scroll.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_missing_vertical_scroll.svg
index c72427de2e..77e21fd0ff 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_missing_vertical_scroll.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_missing_vertical_scroll.svg
@@ -19,138 +19,138 @@
font-weight: 700;
}
- .terminal-1141793223-matrix {
+ .terminal-3070544783-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-1141793223-title {
+ .terminal-3070544783-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-1141793223-r1 { fill: #121212 }
-.terminal-1141793223-r2 { fill: #0178d4 }
-.terminal-1141793223-r3 { fill: #191919 }
-.terminal-1141793223-r4 { fill: #c5c8c6 }
-.terminal-1141793223-r5 { fill: #ddedf9;font-weight: bold }
-.terminal-1141793223-r6 { fill: #1e1e1e }
-.terminal-1141793223-r7 { fill: #e0e0e0 }
-.terminal-1141793223-r8 { fill: #000000 }
+ .terminal-3070544783-r1 { fill: #121212 }
+.terminal-3070544783-r2 { fill: #0178d4 }
+.terminal-3070544783-r3 { fill: #191919 }
+.terminal-3070544783-r4 { fill: #c5c8c6 }
+.terminal-3070544783-r5 { fill: #ddedf9;font-weight: bold }
+.terminal-3070544783-r6 { fill: #1e1e1e }
+.terminal-3070544783-r7 { fill: #e0e0e0 }
+.terminal-3070544783-r8 { fill: #000000 }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- MissingScrollbarApp
+ MissingScrollbarApp
-
-
-
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊0 ▎▊0 ▎▊0 ▎
-▊1 ▎▊1 ▎▊1 ▎
-▊2 ▄▄▎▊2 ▄▄▎▊2 ▄▄▎
-▊3 ▎▊3 ▎▊3 ▎
-▊4 ▎▊4 ▎▊4 ▎
-▊5 ▎▊5 ▎▊5 ▎
-▊6 ▎▊6 ▎▊6 ▎
-▊7 ▎▊7 ▎▊7 ▎
-▊8 ▎▊8 ▎▊8 ▎
-▊9 ▎▊9 ▎▊9 ▎
-▊10 ▎▊10 ▎▊10 ▎
-▊11 ▎▊11 ▎▊11 ▎
-▊12 ▎▊12 ▎▊12 ▎
-▊13 ▎▊13 ▎▊13 ▎
-▊14 ▎▊14 ▎▊14 ▎
-▊15 ▎▊15 ▎▊15 ▎
-▊16 ▎▊16 ▎▊16 ▎
-▊17 ▎▊17 ▎▊17 ▎
-▊18 ▎▊18 ▎▊18 ▎
-▊19 ▎▊19 ▎▊19 ▎
-▊20 ▎▊20 ▎▊20 ▎
-▊21 ▎▊21 ▎▊21 ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊0▎▊0▎▊0▎
+▊1▎▊1▎▊1▎
+▊2▄▄▎▊2▄▄▎▊2▄▄▎
+▊3▎▊3▎▊3▎
+▊4▎▊4▎▊4▎
+▊5▎▊5▎▊5▎
+▊6▎▊6▎▊6▎
+▊7▎▊7▎▊7▎
+▊8▎▊8▎▊8▎
+▊9▎▊9▎▊9▎
+▊10▎▊10▎▊10▎
+▊11▎▊11▎▊11▎
+▊12▎▊12▎▊12▎
+▊13▎▊13▎▊13▎
+▊14▎▊14▎▊14▎
+▊15▎▊15▎▊15▎
+▊16▎▊16▎▊16▎
+▊17▎▊17▎▊17▎
+▊18▎▊18▎▊18▎
+▊19▎▊19▎▊19▎
+▊20▎▊20▎▊20▎
+▊21▎▊21▎▊21▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_build.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_build.svg
index 9cd405e4c4..ac27972501 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_build.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_build.svg
@@ -19,138 +19,138 @@
font-weight: 700;
}
- .terminal-3697314658-matrix {
+ .terminal-242230253-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-3697314658-title {
+ .terminal-242230253-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-3697314658-r1 { fill: #121212 }
-.terminal-3697314658-r2 { fill: #0178d4 }
-.terminal-3697314658-r3 { fill: #191919 }
-.terminal-3697314658-r4 { fill: #c5c8c6 }
-.terminal-3697314658-r5 { fill: #ddedf9;font-weight: bold }
-.terminal-3697314658-r6 { fill: #e0e0e0 }
-.terminal-3697314658-r7 { fill: #424242 }
-.terminal-3697314658-r8 { fill: #3b3b3b }
-.terminal-3697314658-r9 { fill: #f4005f }
+ .terminal-242230253-r1 { fill: #121212 }
+.terminal-242230253-r2 { fill: #0178d4 }
+.terminal-242230253-r3 { fill: #191919 }
+.terminal-242230253-r4 { fill: #c5c8c6 }
+.terminal-242230253-r5 { fill: #ddedf9;font-weight: bold }
+.terminal-242230253-r6 { fill: #e0e0e0 }
+.terminal-242230253-r7 { fill: #424242 }
+.terminal-242230253-r8 { fill: #3b3b3b }
+.terminal-242230253-r9 { fill: #f4005f }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- OptionListApp
+ OptionListApp
-
-
-
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊One ▎▊One ▎▊One ▎
-▊Two ▎▊Two ▎▊Two ▎
-▊──────────────────────▎▊───────────────────────▎▊───────────────────────▎
-▊Three▎▊Three▎▊Three▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊One▎▊One▎▊One▎
+▊Two▎▊Two▎▊Two▎
+▊──────────────────────▎▊───────────────────────▎▊───────────────────────▎
+▊Three▎▊Three▎▊Three▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_options.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_options.svg
index 63dadbfe38..6845477996 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_options.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_options.svg
@@ -19,141 +19,141 @@
font-weight: 700;
}
- .terminal-3327581100-matrix {
+ .terminal-2990244977-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-3327581100-title {
+ .terminal-2990244977-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-3327581100-r1 { fill: #c5c8c6 }
-.terminal-3327581100-r2 { fill: #e0e0e0 }
-.terminal-3327581100-r3 { fill: #121212 }
-.terminal-3327581100-r4 { fill: #0178d4 }
-.terminal-3327581100-r5 { fill: #ddedf9;font-weight: bold }
-.terminal-3327581100-r6 { fill: #272727 }
-.terminal-3327581100-r7 { fill: #424242 }
-.terminal-3327581100-r8 { fill: #797979 }
-.terminal-3327581100-r9 { fill: #000000 }
-.terminal-3327581100-r10 { fill: #495259 }
-.terminal-3327581100-r11 { fill: #ffa62b;font-weight: bold }
+ .terminal-2990244977-r1 { fill: #c5c8c6 }
+.terminal-2990244977-r2 { fill: #e0e0e0 }
+.terminal-2990244977-r3 { fill: #121212 }
+.terminal-2990244977-r4 { fill: #0178d4 }
+.terminal-2990244977-r5 { fill: #ddedf9;font-weight: bold }
+.terminal-2990244977-r6 { fill: #1e1e1e }
+.terminal-2990244977-r7 { fill: #424242 }
+.terminal-2990244977-r8 { fill: #797979 }
+.terminal-2990244977-r9 { fill: #000000 }
+.terminal-2990244977-r10 { fill: #495259 }
+.terminal-2990244977-r11 { fill: #ffa62b;font-weight: bold }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- OptionListApp
+ OptionListApp
-
-
-
- ⭘OptionListApp
-
-
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊Aerilon ▎
-▊Aquaria ▎
-▊──────────────────────────────────────────────────▎
-▊Canceron ▎
-▊Caprica ▎
-▊──────────────────────────────────────────────────▎
-▊Gemenon ▎
-▊──────────────────────────────────────────────────▎
-▊Leonis ▎
-▊Libran ▎
-▊──────────────────────────────────────────────────▎
-▊Picon ▁▁▎
-▊──────────────────────────────────────────────────▎
-▊Sagittaron ▎
-▊Scorpia ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-
-
-
-▏^p palette
+
+
+
+ ⭘OptionListApp
+
+
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊Aerilon▎
+▊Aquaria▎
+▊──────────────────────────────────────────────────▎
+▊Canceron▎
+▊Caprica▎
+▊──────────────────────────────────────────────────▎
+▊Gemenon▎
+▊──────────────────────────────────────────────────▎
+▊Leonis▎
+▊Libran▎
+▊──────────────────────────────────────────────────▎
+▊Picon▁▁▎
+▊──────────────────────────────────────────────────▎
+▊Sagittaron▎
+▊Scorpia▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+▏^p palette
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_replace_prompt_from_single_line_to_single_line.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_replace_prompt_from_single_line_to_single_line.svg
index f31d1be961..4800f408fa 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_replace_prompt_from_single_line_to_single_line.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_replace_prompt_from_single_line_to_single_line.svg
@@ -19,137 +19,137 @@
font-weight: 700;
}
- .terminal-265082841-matrix {
+ .terminal-731738018-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-265082841-title {
+ .terminal-731738018-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-265082841-r1 { fill: #c5c8c6 }
-.terminal-265082841-r2 { fill: #e0e0e0 }
-.terminal-265082841-r3 { fill: #121212 }
-.terminal-265082841-r4 { fill: #0178d4 }
-.terminal-265082841-r5 { fill: #ddedf9;font-weight: bold }
-.terminal-265082841-r6 { fill: #495259 }
-.terminal-265082841-r7 { fill: #ffa62b;font-weight: bold }
+ .terminal-731738018-r1 { fill: #c5c8c6 }
+.terminal-731738018-r2 { fill: #e0e0e0 }
+.terminal-731738018-r3 { fill: #121212 }
+.terminal-731738018-r4 { fill: #0178d4 }
+.terminal-731738018-r5 { fill: #ddedf9;font-weight: bold }
+.terminal-731738018-r6 { fill: #495259 }
+.terminal-731738018-r7 { fill: #ffa62b;font-weight: bold }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- OptionListApp
+ OptionListApp
-
-
-
- ⭘OptionListApp
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊1. Another single line ▎
-▊2. Two ▎
-▊lines ▎
-▊3. Three ▎
-▊lines ▎
-▊of text ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-▏^p palette
+
+
+
+ ⭘OptionListApp
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊1. Another single line▎
+▊2. Two▎
+▊lines▎
+▊3. Three▎
+▊lines▎
+▊of text▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+▏^p palette
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_replace_prompt_from_single_line_to_two_lines.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_replace_prompt_from_single_line_to_two_lines.svg
index c8b8a5369f..8f2417074c 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_replace_prompt_from_single_line_to_two_lines.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_replace_prompt_from_single_line_to_two_lines.svg
@@ -19,137 +19,137 @@
font-weight: 700;
}
- .terminal-4082085973-matrix {
+ .terminal-2447019988-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-4082085973-title {
+ .terminal-2447019988-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-4082085973-r1 { fill: #c5c8c6 }
-.terminal-4082085973-r2 { fill: #e0e0e0 }
-.terminal-4082085973-r3 { fill: #121212 }
-.terminal-4082085973-r4 { fill: #0178d4 }
-.terminal-4082085973-r5 { fill: #ddedf9;font-weight: bold }
-.terminal-4082085973-r6 { fill: #495259 }
-.terminal-4082085973-r7 { fill: #ffa62b;font-weight: bold }
+ .terminal-2447019988-r1 { fill: #c5c8c6 }
+.terminal-2447019988-r2 { fill: #e0e0e0 }
+.terminal-2447019988-r3 { fill: #121212 }
+.terminal-2447019988-r4 { fill: #0178d4 }
+.terminal-2447019988-r5 { fill: #ddedf9;font-weight: bold }
+.terminal-2447019988-r6 { fill: #495259 }
+.terminal-2447019988-r7 { fill: #ffa62b;font-weight: bold }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- OptionListApp
+ OptionListApp
-
-
-
- ⭘OptionListApp
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊1. Two ▎
-▊lines ▎
-▊2. Two ▎
-▊lines ▎
-▊3. Three ▎
-▊lines ▎
-▊of text ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-
-
-
-
-
-
-
-
-
-
-
-
-
-▏^p palette
+
+
+
+ ⭘OptionListApp
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊1. Two▎
+▊lines▎
+▊2. Two▎
+▊lines▎
+▊3. Three▎
+▊lines▎
+▊of text▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+
+
+
+
+
+
+
+
+
+
+▏^p palette
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_replace_prompt_from_two_lines_to_three_lines.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_replace_prompt_from_two_lines_to_three_lines.svg
index 27794d3b71..c98c21136d 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_replace_prompt_from_two_lines_to_three_lines.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_replace_prompt_from_two_lines_to_three_lines.svg
@@ -19,137 +19,137 @@
font-weight: 700;
}
- .terminal-1539092124-matrix {
+ .terminal-741247382-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-1539092124-title {
+ .terminal-741247382-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-1539092124-r1 { fill: #c5c8c6 }
-.terminal-1539092124-r2 { fill: #e0e0e0 }
-.terminal-1539092124-r3 { fill: #121212 }
-.terminal-1539092124-r4 { fill: #0178d4 }
-.terminal-1539092124-r5 { fill: #ddedf9;font-weight: bold }
-.terminal-1539092124-r6 { fill: #495259 }
-.terminal-1539092124-r7 { fill: #ffa62b;font-weight: bold }
+ .terminal-741247382-r1 { fill: #c5c8c6 }
+.terminal-741247382-r2 { fill: #e0e0e0 }
+.terminal-741247382-r3 { fill: #121212 }
+.terminal-741247382-r4 { fill: #0178d4 }
+.terminal-741247382-r5 { fill: #ddedf9;font-weight: bold }
+.terminal-741247382-r6 { fill: #495259 }
+.terminal-741247382-r7 { fill: #ffa62b;font-weight: bold }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- OptionListApp
+ OptionListApp
-
-
-
- ⭘OptionListApp
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊1. Single line ▎
-▊1. Three ▎
-▊lines ▎
-▊of text ▎
-▊3. Three ▎
-▊lines ▎
-▊of text ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-
-
-
-
-
-
-
-
-
-
-
-
-
-▏^p palette
+
+
+
+ ⭘OptionListApp
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊1. Single line▎
+▊1. Three▎
+▊lines▎
+▊of text▎
+▊3. Three▎
+▊lines▎
+▊of text▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+
+
+
+
+
+
+
+
+
+
+▏^p palette
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_scrolling_in_long_list.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_scrolling_in_long_list.svg
index b26c1cb414..f62e650899 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_scrolling_in_long_list.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_scrolling_in_long_list.svg
@@ -19,137 +19,137 @@
font-weight: 700;
}
- .terminal-3165090202-matrix {
+ .terminal-3874045103-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-3165090202-title {
+ .terminal-3874045103-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-3165090202-r1 { fill: #121212 }
-.terminal-3165090202-r2 { fill: #0178d4 }
-.terminal-3165090202-r3 { fill: #c5c8c6 }
-.terminal-3165090202-r4 { fill: #e0e0e0 }
-.terminal-3165090202-r5 { fill: #003054 }
-.terminal-3165090202-r6 { fill: #272727 }
-.terminal-3165090202-r7 { fill: #ddedf9;font-weight: bold }
+ .terminal-3874045103-r1 { fill: #121212 }
+.terminal-3874045103-r2 { fill: #0178d4 }
+.terminal-3874045103-r3 { fill: #c5c8c6 }
+.terminal-3874045103-r4 { fill: #e0e0e0 }
+.terminal-3874045103-r5 { fill: #003054 }
+.terminal-3874045103-r6 { fill: #272727 }
+.terminal-3874045103-r7 { fill: #ddedf9;font-weight: bold }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- LongOptionListApp
+ LongOptionListApp
-
-
-
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊This is option #78 ▎
-▊This is option #79 ▎
-▊This is option #80 ▎
-▊This is option #81 ▎
-▊This is option #82 ▎
-▊This is option #83 ▎
-▊This is option #84 ▎
-▊This is option #85 ▎
-▊This is option #86 ▎
-▊This is option #87 ▎
-▊This is option #88 ▎
-▊This is option #89 ▎
-▊This is option #90 ▎
-▊This is option #91 ▎
-▊This is option #92 ▎
-▊This is option #93 ▎
-▊This is option #94 ▎
-▊This is option #95 ▇▇▎
-▊This is option #96 ▎
-▊This is option #97 ▎
-▊This is option #98 ▎
-▊This is option #99 ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊This is option #78▎
+▊This is option #79▎
+▊This is option #80▎
+▊This is option #81▎
+▊This is option #82▎
+▊This is option #83▎
+▊This is option #84▎
+▊This is option #85▎
+▊This is option #86▎
+▊This is option #87▎
+▊This is option #88▎
+▊This is option #89▎
+▊This is option #90▎
+▊This is option #91▎
+▊This is option #92▎
+▊This is option #93▎
+▊This is option #94▎
+▊This is option #95▇▇▎
+▊This is option #96▎
+▊This is option #97▎
+▊This is option #98▎
+▊This is option #99▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_strings.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_strings.svg
index 5b80b97f2e..70a6045b27 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_strings.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_strings.svg
@@ -19,137 +19,137 @@
font-weight: 700;
}
- .terminal-2168702108-matrix {
+ .terminal-2124869530-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-2168702108-title {
+ .terminal-2124869530-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-2168702108-r1 { fill: #c5c8c6 }
-.terminal-2168702108-r2 { fill: #e0e0e0 }
-.terminal-2168702108-r3 { fill: #121212 }
-.terminal-2168702108-r4 { fill: #0178d4 }
-.terminal-2168702108-r5 { fill: #ddedf9;font-weight: bold }
-.terminal-2168702108-r6 { fill: #495259 }
-.terminal-2168702108-r7 { fill: #ffa62b;font-weight: bold }
+ .terminal-2124869530-r1 { fill: #c5c8c6 }
+.terminal-2124869530-r2 { fill: #e0e0e0 }
+.terminal-2124869530-r3 { fill: #121212 }
+.terminal-2124869530-r4 { fill: #0178d4 }
+.terminal-2124869530-r5 { fill: #ddedf9;font-weight: bold }
+.terminal-2124869530-r6 { fill: #495259 }
+.terminal-2124869530-r7 { fill: #ffa62b;font-weight: bold }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- OptionListApp
+ OptionListApp
-
-
-
- ⭘OptionListApp
-
-
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊Aerilon ▎
-▊Aquaria ▎
-▊Canceron ▎
-▊Caprica ▎
-▊Gemenon ▎
-▊Leonis ▎
-▊Libran ▎
-▊Picon ▎
-▊Sagittaron ▎
-▊Scorpia ▎
-▊Tauron ▎
-▊Virgon ▎
-▊▎
-▊▎
-▊▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-
-
-
-▏^p palette
+
+
+
+ ⭘OptionListApp
+
+
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊Aerilon▎
+▊Aquaria▎
+▊Canceron▎
+▊Caprica▎
+▊Gemenon▎
+▊Leonis▎
+▊Libran▎
+▊Picon▎
+▊Sagittaron▎
+▊Scorpia▎
+▊Tauron▎
+▊Virgon▎
+▊▎
+▊▎
+▊▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+▏^p palette
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_wrapping.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_wrapping.svg
new file mode 100644
index 0000000000..1e1758afbb
--- /dev/null
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_option_list_wrapping.svg
@@ -0,0 +1,153 @@
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_expanded.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_expanded.svg
index c236117156..3d958528f7 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_expanded.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_expanded.svg
@@ -19,137 +19,137 @@
font-weight: 700;
}
- .terminal-764844436-matrix {
+ .terminal-1421459469-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-764844436-title {
+ .terminal-1421459469-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-764844436-r1 { fill: #c5c8c6 }
-.terminal-764844436-r2 { fill: #e0e0e0 }
-.terminal-764844436-r3 { fill: #121212 }
-.terminal-764844436-r4 { fill: #191919 }
-.terminal-764844436-r5 { fill: #7f7f7f }
-.terminal-764844436-r6 { fill: #0178d4 }
-.terminal-764844436-r7 { fill: #ddedf9;font-weight: bold }
-.terminal-764844436-r8 { fill: #85beea;font-weight: bold }
+ .terminal-1421459469-r1 { fill: #c5c8c6 }
+.terminal-1421459469-r2 { fill: #e0e0e0 }
+.terminal-1421459469-r3 { fill: #121212 }
+.terminal-1421459469-r4 { fill: #191919 }
+.terminal-1421459469-r5 { fill: #7f7f7f }
+.terminal-1421459469-r6 { fill: #0178d4 }
+.terminal-1421459469-r7 { fill: #ddedf9;font-weight: bold }
+.terminal-1421459469-r8 { fill: #85beea;font-weight: bold }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- SelectApp
+ SelectApp
-
-
-
- ⭘SelectApp
-
-
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊Select▲▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊Select▎
-▊ I must not fear. ▎
-▊ Fear is the mind-killer. ▎
-▊ 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. ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-
-
-
-
-
-
-
-
+
+
+
+ ⭘SelectApp
+
+
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊Select▲▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊Select▎
+▊I must not fear.▎
+▊Fear is the mind-killer.▎
+▊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.▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+
+
+
+
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_from_values_expanded.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_from_values_expanded.svg
index c236117156..3d958528f7 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_from_values_expanded.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_from_values_expanded.svg
@@ -19,137 +19,137 @@
font-weight: 700;
}
- .terminal-764844436-matrix {
+ .terminal-1421459469-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-764844436-title {
+ .terminal-1421459469-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-764844436-r1 { fill: #c5c8c6 }
-.terminal-764844436-r2 { fill: #e0e0e0 }
-.terminal-764844436-r3 { fill: #121212 }
-.terminal-764844436-r4 { fill: #191919 }
-.terminal-764844436-r5 { fill: #7f7f7f }
-.terminal-764844436-r6 { fill: #0178d4 }
-.terminal-764844436-r7 { fill: #ddedf9;font-weight: bold }
-.terminal-764844436-r8 { fill: #85beea;font-weight: bold }
+ .terminal-1421459469-r1 { fill: #c5c8c6 }
+.terminal-1421459469-r2 { fill: #e0e0e0 }
+.terminal-1421459469-r3 { fill: #121212 }
+.terminal-1421459469-r4 { fill: #191919 }
+.terminal-1421459469-r5 { fill: #7f7f7f }
+.terminal-1421459469-r6 { fill: #0178d4 }
+.terminal-1421459469-r7 { fill: #ddedf9;font-weight: bold }
+.terminal-1421459469-r8 { fill: #85beea;font-weight: bold }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- SelectApp
+ SelectApp
-
-
-
- ⭘SelectApp
-
-
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊Select▲▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊Select▎
-▊ I must not fear. ▎
-▊ Fear is the mind-killer. ▎
-▊ 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. ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-
-
-
-
-
-
-
-
+
+
+
+ ⭘SelectApp
+
+
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊Select▲▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊Select▎
+▊I must not fear.▎
+▊Fear is the mind-killer.▎
+▊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.▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+
+
+
+
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_overlay_constrain.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_overlay_constrain.svg
index 7517b110b2..58af6a4242 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_overlay_constrain.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_overlay_constrain.svg
@@ -19,138 +19,138 @@
font-weight: 700;
}
- .terminal-1279757937-matrix {
+ .terminal-805783598-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-1279757937-title {
+ .terminal-805783598-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-1279757937-r1 { fill: #121212 }
-.terminal-1279757937-r2 { fill: #ffffff }
-.terminal-1279757937-r3 { fill: #e0e0e0 }
-.terminal-1279757937-r4 { fill: #c5c8c6 }
-.terminal-1279757937-r5 { fill: #0178d4 }
-.terminal-1279757937-r6 { fill: #969696 }
-.terminal-1279757937-r7 { fill: #272727 }
-.terminal-1279757937-r8 { fill: #000000 }
+ .terminal-805783598-r1 { fill: #121212 }
+.terminal-805783598-r2 { fill: #ffffff }
+.terminal-805783598-r3 { fill: #e0e0e0 }
+.terminal-805783598-r4 { fill: #c5c8c6 }
+.terminal-805783598-r5 { fill: #0178d4 }
+.terminal-805783598-r6 { fill: #969696 }
+.terminal-805783598-r7 { fill: #272727 }
+.terminal-805783598-r8 { fill: #000000 }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- OApp
+ OApp
-
-
-
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊Padding (ignore)▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▎
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊Select▎
-▊ Foo ▎
-▊ bar ▎
-▊ baz ▆▆▎
-▊ Foo ▎
-▊ bar ▎
-▊ baz ▎
-▊ Foo ▎
-▊ bar ▎
-▊ baz ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊Padding (ignore)▎
+▊▎
+▊▎
+▊▎
+▊▎
+▊▎
+▊▎
+▊▎
+▊▎
+▊▎
+▊▎
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊Select▎
+▊Foo▎
+▊bar▎
+▊baz▆▆▎
+▊Foo▎
+▊bar▎
+▊baz▎
+▊Foo▎
+▊bar▎
+▊baz▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_rebuild.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_rebuild.svg
index 042a5c38f2..3678528950 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_rebuild.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_rebuild.svg
@@ -19,137 +19,137 @@
font-weight: 700;
}
- .terminal-796561727-matrix {
+ .terminal-2896938123-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-796561727-title {
+ .terminal-2896938123-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-796561727-r1 { fill: #121212 }
-.terminal-796561727-r2 { fill: #191919 }
-.terminal-796561727-r3 { fill: #c5c8c6 }
-.terminal-796561727-r4 { fill: #7f7f7f }
-.terminal-796561727-r5 { fill: #0178d4 }
-.terminal-796561727-r6 { fill: #ddedf9;font-weight: bold }
-.terminal-796561727-r7 { fill: #85beea;font-weight: bold }
-.terminal-796561727-r8 { fill: #e0e0e0 }
+ .terminal-2896938123-r1 { fill: #121212 }
+.terminal-2896938123-r2 { fill: #191919 }
+.terminal-2896938123-r3 { fill: #c5c8c6 }
+.terminal-2896938123-r4 { fill: #7f7f7f }
+.terminal-2896938123-r5 { fill: #0178d4 }
+.terminal-2896938123-r6 { fill: #ddedf9;font-weight: bold }
+.terminal-2896938123-r7 { fill: #85beea;font-weight: bold }
+.terminal-2896938123-r8 { fill: #e0e0e0 }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- SelectRebuildApp
+ SelectRebuildApp
-
-
-
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊Select▲▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊Select▎
-▊ This ▎
-▊ Should ▎
-▊ Be ▎
-▊ What ▎
-▊ Goes ▎
-▊ Into ▎
-▊ The ▎
-▊ Snapshit ▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-
-
-
-
-
-
-
-
-
+
+
+
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊Select▲▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊Select▎
+▊This▎
+▊Should▎
+▊Be▎
+▊What▎
+▊Goes▎
+▊Into▎
+▊The▎
+▊Snapshit▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+
+
+
+
+
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_type_to_search.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_type_to_search.svg
index 47d0ba6c63..3e0066f216 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_type_to_search.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_type_to_search.svg
@@ -19,138 +19,138 @@
font-weight: 700;
}
- .terminal-701088566-matrix {
+ .terminal-4074332036-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-701088566-title {
+ .terminal-4074332036-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-701088566-r1 { fill: #121212 }
-.terminal-701088566-r2 { fill: #191919 }
-.terminal-701088566-r3 { fill: #c5c8c6 }
-.terminal-701088566-r4 { fill: #7f7f7f }
-.terminal-701088566-r5 { fill: #0178d4 }
-.terminal-701088566-r6 { fill: #e0e0e0 }
-.terminal-701088566-r7 { fill: #003054 }
-.terminal-701088566-r8 { fill: #ddedf9;font-weight: bold }
-.terminal-701088566-r9 { fill: #000000 }
+ .terminal-4074332036-r1 { fill: #121212 }
+.terminal-4074332036-r2 { fill: #191919 }
+.terminal-4074332036-r3 { fill: #c5c8c6 }
+.terminal-4074332036-r4 { fill: #7f7f7f }
+.terminal-4074332036-r5 { fill: #0178d4 }
+.terminal-4074332036-r6 { fill: #e0e0e0 }
+.terminal-4074332036-r7 { fill: #003054 }
+.terminal-4074332036-r8 { fill: #ddedf9;font-weight: bold }
+.terminal-4074332036-r9 { fill: #000000 }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- SelectTypeToSearch
+ SelectTypeToSearch
-
-
-
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊Select▲▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊ Chicken ▎
-▊ Goose ▄▄▎
-▊ Pigeon ▃▃▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊Select▲▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊Chicken▎
+▊Goose▄▄▎
+▊Pigeon▃▃▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_width_auto.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_width_auto.svg
deleted file mode 100644
index 2205679653..0000000000
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_select_width_auto.svg
+++ /dev/null
@@ -1,158 +0,0 @@
-
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_textual_dev_border_preview.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_textual_dev_border_preview.svg
index 634d36dc04..0e6de792a3 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_textual_dev_border_preview.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_textual_dev_border_preview.svg
@@ -19,136 +19,136 @@
font-weight: 700;
}
- .terminal-2476667460-matrix {
+ .terminal-3794448518-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-2476667460-title {
+ .terminal-3794448518-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-2476667460-r1 { fill: #121212 }
-.terminal-2476667460-r2 { fill: #0178d4 }
-.terminal-2476667460-r3 { fill: #e0e0e0 }
-.terminal-2476667460-r4 { fill: #c5c8c6 }
-.terminal-2476667460-r5 { fill: #ddedf9;font-weight: bold }
-.terminal-2476667460-r6 { fill: #e2e3e5 }
+ .terminal-3794448518-r1 { fill: #121212 }
+.terminal-3794448518-r2 { fill: #0178d4 }
+.terminal-3794448518-r3 { fill: #e0e0e0 }
+.terminal-3794448518-r4 { fill: #c5c8c6 }
+.terminal-3794448518-r5 { fill: #ddedf9;font-weight: bold }
+.terminal-3794448518-r6 { fill: #e2e3e5 }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- BorderApp
+ BorderApp
-
-
-
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
-▊ascii ▎
-▊blank ▎
-▊dashed ▎+--------------------- ascii ----------------------+
-▊double ▎||
-▊heavy ▎||
-▊hidden ▎|I must not fear.|
-▊hkey ▎|Fear is the mind-killer.|
-▊inner ▎|Fear is the little-death that brings total|
-▊none ▎|obliteration.|
-▊outer ▎|I will face my fear.|
-▊panel ▎|I will permit it to pass over me and |
-▊round ▎|through me.|
-▊solid ▎|And when it has gone past, I will turn the|
-▊tab ▎|inner eye to see its path.|
-▊tall ▎|Where the fear has gone there will be |
-▊thick ▎|nothing. Only I will remain.|
-▊vkey ▎||
-▊wide ▎||
-▊▎+-------------------------------- border subtitle -+
-▊▎
-▊▎
-▊▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+
+
+
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+▊ascii▎
+▊blank▎
+▊dashed▎+--------------------- ascii ----------------------+
+▊double▎||
+▊heavy▎||
+▊hidden▎|I must not fear.|
+▊hkey▎|Fear is the mind-killer.|
+▊inner▎|Fear is the little-death that brings total|
+▊none▎|obliteration.|
+▊outer▎|I will face my fear.|
+▊panel▎|I will permit it to pass over me and |
+▊round▎|through me.|
+▊solid▎|And when it has gone past, I will turn the|
+▊tab▎|inner eye to see its path.|
+▊tall▎|Where the fear has gone there will be |
+▊thick▎|nothing. Only I will remain.|
+▊vkey▎||
+▊wide▎||
+▊▎+-------------------------------- border subtitle -+
+▊▎
+▊▎
+▊▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_textual_dev_colors_preview.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_textual_dev_colors_preview.svg
index 827c349428..3aa56a354d 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_textual_dev_colors_preview.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_textual_dev_colors_preview.svg
@@ -19,150 +19,150 @@
font-weight: 700;
}
- .terminal-4282265730-matrix {
+ .terminal-171841527-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-4282265730-title {
+ .terminal-171841527-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-4282265730-r1 { fill: #e0e0e0 }
-.terminal-4282265730-r2 { fill: #c5c8c6 }
-.terminal-4282265730-r3 { fill: #ddedf9;font-weight: bold }
-.terminal-4282265730-r4 { fill: #797979 }
-.terminal-4282265730-r5 { fill: #4f4f4f }
-.terminal-4282265730-r6 { fill: #0178d4 }
-.terminal-4282265730-r7 { fill: #121212 }
-.terminal-4282265730-r8 { fill: #000000 }
-.terminal-4282265730-r9 { fill: #e1e1e1;font-weight: bold }
-.terminal-4282265730-r10 { fill: #dde6f1 }
-.terminal-4282265730-r11 { fill: #99b3d4 }
-.terminal-4282265730-r12 { fill: #dde8f3 }
-.terminal-4282265730-r13 { fill: #99badd }
-.terminal-4282265730-r14 { fill: #ddeaf6 }
-.terminal-4282265730-r15 { fill: #99c1e5 }
-.terminal-4282265730-r16 { fill: #ddedf9 }
-.terminal-4282265730-r17 { fill: #99c9ed }
-.terminal-4282265730-r18 { fill: #003054 }
-.terminal-4282265730-r19 { fill: #ffa62b;font-weight: bold }
-.terminal-4282265730-r20 { fill: #495259 }
+ .terminal-171841527-r1 { fill: #e0e0e0 }
+.terminal-171841527-r2 { fill: #c5c8c6 }
+.terminal-171841527-r3 { fill: #ddedf9;font-weight: bold }
+.terminal-171841527-r4 { fill: #797979 }
+.terminal-171841527-r5 { fill: #4f4f4f }
+.terminal-171841527-r6 { fill: #0178d4 }
+.terminal-171841527-r7 { fill: #121212 }
+.terminal-171841527-r8 { fill: #000000 }
+.terminal-171841527-r9 { fill: #e1e1e1;font-weight: bold }
+.terminal-171841527-r10 { fill: #dde6f1 }
+.terminal-171841527-r11 { fill: #99b3d4 }
+.terminal-171841527-r12 { fill: #dde8f3 }
+.terminal-171841527-r13 { fill: #99badd }
+.terminal-171841527-r14 { fill: #ddeaf6 }
+.terminal-171841527-r15 { fill: #99c1e5 }
+.terminal-171841527-r16 { fill: #ddedf9 }
+.terminal-171841527-r17 { fill: #99c9ed }
+.terminal-171841527-r18 { fill: #003054 }
+.terminal-171841527-r19 { fill: #ffa62b;font-weight: bold }
+.terminal-171841527-r20 { fill: #495259 }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- ColorsApp
+ ColorsApp
-
-
-
-
-Theme ColorsNamed Colors
-╸━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
-▊█ Theme Colors █████████▎
-▊▎
-▊primary ▎▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
-▊secondary ▎▎
-▊background ▎▎"primary"
-▊primary-background ▎▎
-▊secondary-background▎▎
-▊surface ▎▎$primary-darken-3$text-mute
-▊panel ▎▎
-▊boost ▎▎
-▊warning ▎▎$primary-darken-2$text-mute
-▊error ▎▎
-▊success ▎▎
-▊accent ▎▎$primary-darken-1$text-mute
-▊▎▎
-▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▎
-▎$primary$text-mute
-▎
-▉
- [ Previous theme ] Next theme ▏^p palette
+
+
+
+
+Theme ColorsNamed Colors
+╸━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+▊█ Theme Colors █████████▎
+▊▎
+▊primary▎▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
+▊secondary▎▎
+▊background▎▎"primary"
+▊primary-background▎▎
+▊secondary-background▎▎
+▊surface▎▎$primary-darken-3$text-mute
+▊panel▎▎
+▊boost▎▎
+▊warning▎▎$primary-darken-2$text-mute
+▊error▎▎
+▊success▎▎
+▊accent▎▎$primary-darken-1$text-mute
+▊▎▎
+▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎▎
+▎$primary$text-mute
+▎
+▉
+ [ Previous theme ] Next theme ▏^p palette
diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py
index 0c0cb70f2e..7390b20d0b 100644
--- a/tests/snapshot_tests/test_snapshots.py
+++ b/tests/snapshot_tests/test_snapshots.py
@@ -8,6 +8,7 @@
from tests.snapshot_tests.language_snippets import SNIPPETS
from textual import events
+from textual._on import on
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.command import SimpleCommand
@@ -15,6 +16,7 @@
Center,
Container,
Grid,
+ Horizontal,
Middle,
Vertical,
VerticalGroup,
@@ -22,6 +24,7 @@
HorizontalGroup,
)
from textual.pilot import Pilot
+from textual.reactive import var
from textual.renderables.gradient import LinearGradient
from textual.screen import ModalScreen, Screen
from textual.widgets import (
@@ -2851,6 +2854,8 @@ def compose(self) -> ComposeResult:
assert snap_compare(GridOffsetApp())
+# Figure out why this test is flakey
+@pytest.mark.skip("This test is flakey (why)?")
def test_select_width_auto(snap_compare):
"""Regression test for https://github.com/Textualize/textual/issues/5280"
The overlay has a width of auto, so the first (widest) option should not wrap."""
@@ -3421,3 +3426,126 @@ def compose(self) -> ComposeResult:
yield Label(TEXT, id="label3")
assert snap_compare(OverflowApp())
+
+
+def test_empty_option_list(snap_compare):
+ """Regression test for https://github.com/Textualize/textual/issues/5489
+
+ You should see an OptionList with no options, resulting in a small square at the top left.
+
+ """
+
+ class OptionListAutoCrash(App[None]):
+
+ CSS = """
+ OptionList {
+ width: auto;
+ }
+ """
+
+ def compose(self) -> ComposeResult:
+ yield OptionList()
+
+ snap_compare(OptionListAutoCrash())
+
+
+def test_focus_within_transparent(snap_compare):
+ """Regression test for https://github.com/Textualize/textual/issues/5488
+
+ You should see the right 50% in yellow, with a yellow OptionList and a black TextArea
+ """
+
+ class Panel(Vertical, can_focus=True):
+ pass
+
+ class FocusWithinTransparentApp(App[None]):
+
+ CSS = """
+ Screen {
+ layout: horizontal;
+ }
+
+ Input {
+ width: 1fr;
+ height: 1fr;
+ }
+
+ Panel {
+ padding: 5 10;
+ background: red;
+ &:focus, &:focus-within {
+ background: yellow;
+ }
+
+ OptionList, OptionList:focus {
+ height: 1fr;
+ background: transparent;
+ }
+ }
+ """
+
+ def compose(self) -> ComposeResult:
+ yield Input(placeholder="This is here to escape to")
+ with Panel():
+ yield OptionList(*["This is an option" for _ in range(30)])
+ yield Input(placeholder="Escape out via here for the bug")
+
+ snap_compare(FocusWithinTransparentApp(), press=["tab"])
+
+
+def test_option_list_wrapping(snap_compare):
+ """You should see a 40 cell wide Option list with a single line, ending in an ellipsis."""
+
+ class OLApp(App):
+ CSS = """
+ OptionList {
+ width: 40;
+ text-wrap: nowrap;
+ text-overflow: ellipsis;
+ }
+ """
+
+ def compose(self) -> ComposeResult:
+ yield OptionList(
+ "This is a very long option that is too wide to fit within the space provided and will overflow."
+ )
+
+ snap_compare(OLApp())
+
+
+def test_add_separator(snap_compare):
+ """Regression test for https://github.com/Textualize/textual/issues/5431
+
+ You should see a button on the left. On the right an option list with Option 1, separator, Option 3
+
+ """
+
+ class FocusTest(App[None]):
+
+ CSS = """
+ OptionList {
+ height: 1fr;
+ }
+ """
+
+ counter: var[int] = var(0)
+
+ def compose(self) -> ComposeResult:
+ with Horizontal():
+ yield Button("Add")
+ yield OptionList()
+
+ @on(Button.Pressed)
+ def add_more_stuff(self) -> None:
+ self.counter += 1
+ self.query_one(OptionList).add_option(
+ (f"This is option {self.counter}" if self.counter % 2 else None)
+ )
+
+ async def run_before(pilot: Pilot) -> None:
+ await pilot.pause()
+ for _ in range(3):
+ await pilot.click(Button)
+ await pilot.pause(0.4)
+
+ snap_compare(FocusTest(), run_before=run_before)
diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py
index dc3c8ccd92..59f3c458d5 100644
--- a/tests/test_fuzzy.py
+++ b/tests/test_fuzzy.py
@@ -1,7 +1,6 @@
-from rich.style import Style
-from rich.text import Span
-
+from textual.content import Span
from textual.fuzzy import Matcher
+from textual.style import Style
def test_no_match():
@@ -28,6 +27,7 @@ def test_highlight():
matcher = Matcher("foo.bar")
spans = matcher.highlight("foo/egg.bar").spans
+ print(repr(spans))
assert spans == [
Span(0, 1, Style(reverse=True)),
Span(1, 2, Style(reverse=True)),