269 lines
7.3 KiB
Python
269 lines
7.3 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Iterable
|
|
|
|
from rich.console import Console, ConsoleOptions
|
|
from rich.segment import Segment
|
|
from rich.style import Style
|
|
from typing_extensions import Literal
|
|
|
|
from textual.app import RenderResult
|
|
from textual.css._error_tools import friendly_list
|
|
from textual.geometry import Size
|
|
from textual.reactive import Reactive, reactive
|
|
from textual.widget import Widget
|
|
|
|
RuleOrientation = Literal["horizontal", "vertical"]
|
|
"""The valid orientations of the rule widget."""
|
|
|
|
LineStyle = Literal[
|
|
"ascii",
|
|
"blank",
|
|
"dashed",
|
|
"double",
|
|
"heavy",
|
|
"hidden",
|
|
"none",
|
|
"solid",
|
|
"thick",
|
|
]
|
|
"""The valid line styles of the rule widget."""
|
|
|
|
|
|
_VALID_RULE_ORIENTATIONS = {"horizontal", "vertical"}
|
|
|
|
_VALID_LINE_STYLES = {
|
|
"ascii",
|
|
"blank",
|
|
"dashed",
|
|
"double",
|
|
"heavy",
|
|
"hidden",
|
|
"none",
|
|
"solid",
|
|
"thick",
|
|
}
|
|
|
|
_HORIZONTAL_LINE_CHARS: dict[LineStyle, str] = {
|
|
"ascii": "-",
|
|
"blank": " ",
|
|
"dashed": "╍",
|
|
"double": "═",
|
|
"heavy": "━",
|
|
"hidden": " ",
|
|
"none": " ",
|
|
"solid": "─",
|
|
"thick": "█",
|
|
}
|
|
|
|
_VERTICAL_LINE_CHARS: dict[LineStyle, str] = {
|
|
"ascii": "|",
|
|
"blank": " ",
|
|
"dashed": "╏",
|
|
"double": "║",
|
|
"heavy": "┃",
|
|
"hidden": " ",
|
|
"none": " ",
|
|
"solid": "│",
|
|
"thick": "█",
|
|
}
|
|
|
|
|
|
class InvalidRuleOrientation(Exception):
|
|
"""Exception raised for an invalid rule orientation."""
|
|
|
|
|
|
class InvalidLineStyle(Exception):
|
|
"""Exception raised for an invalid rule line style."""
|
|
|
|
|
|
class HorizontalRuleRenderable:
|
|
"""Renders a horizontal rule."""
|
|
|
|
def __init__(self, character: str, style: Style, width: int):
|
|
self.character = character
|
|
self.style = style
|
|
self.width = width
|
|
|
|
def __rich_console__(
|
|
self, console: Console, options: ConsoleOptions
|
|
) -> Iterable[Segment]:
|
|
yield Segment(self.width * self.character, self.style)
|
|
|
|
|
|
class VerticalRuleRenderable:
|
|
"""Renders a vertical rule."""
|
|
|
|
def __init__(self, character: str, style: Style, height: int):
|
|
self.character = character
|
|
self.style = style
|
|
self.height = height
|
|
|
|
def __rich_console__(
|
|
self, console: Console, options: ConsoleOptions
|
|
) -> Iterable[Segment]:
|
|
segment = Segment(self.character, self.style)
|
|
new_line = Segment.line()
|
|
return ([segment, new_line] * self.height)[:-1]
|
|
|
|
|
|
class Rule(Widget, can_focus=False):
|
|
"""A rule widget to separate content, similar to a `<hr>` HTML tag."""
|
|
|
|
DEFAULT_CSS = """
|
|
Rule {
|
|
color: $secondary;
|
|
}
|
|
|
|
Rule.-horizontal {
|
|
height: 1;
|
|
margin: 1 0;
|
|
width: 1fr;
|
|
}
|
|
|
|
Rule.-vertical {
|
|
width: 1;
|
|
margin: 0 2;
|
|
height: 1fr;
|
|
}
|
|
"""
|
|
|
|
orientation: Reactive[RuleOrientation] = reactive[RuleOrientation]("horizontal")
|
|
"""The orientation of the rule."""
|
|
|
|
line_style: Reactive[LineStyle] = reactive[LineStyle]("solid")
|
|
"""The line style of the rule."""
|
|
|
|
def __init__(
|
|
self,
|
|
orientation: RuleOrientation = "horizontal",
|
|
line_style: LineStyle = "solid",
|
|
*,
|
|
name: str | None = None,
|
|
id: str | None = None,
|
|
classes: str | None = None,
|
|
disabled: bool = False,
|
|
) -> None:
|
|
"""Initialize a rule widget.
|
|
|
|
Args:
|
|
orientation: The orientation of the rule.
|
|
line_style: The line style of the rule.
|
|
name: The name of the widget.
|
|
id: The ID of the widget in the DOM.
|
|
classes: The CSS classes of the widget.
|
|
disabled: Whether the widget is disabled or not.
|
|
"""
|
|
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
|
|
self.orientation = orientation
|
|
self.line_style = line_style
|
|
self.expand = True
|
|
|
|
def render(self) -> RenderResult:
|
|
rule_character: str
|
|
style = self.rich_style
|
|
if self.orientation == "vertical":
|
|
rule_character = _VERTICAL_LINE_CHARS[self.line_style]
|
|
return VerticalRuleRenderable(
|
|
rule_character, style, self.content_size.height
|
|
)
|
|
elif self.orientation == "horizontal":
|
|
rule_character = _HORIZONTAL_LINE_CHARS[self.line_style]
|
|
return HorizontalRuleRenderable(
|
|
rule_character, style, self.content_size.width
|
|
)
|
|
else:
|
|
raise InvalidRuleOrientation(
|
|
f"Valid rule orientations are {friendly_list(_VALID_RULE_ORIENTATIONS)}"
|
|
)
|
|
|
|
def watch_orientation(
|
|
self, old_orientation: RuleOrientation, orientation: RuleOrientation
|
|
) -> None:
|
|
self.remove_class(f"-{old_orientation}")
|
|
self.add_class(f"-{orientation}")
|
|
|
|
def validate_orientation(self, orientation: RuleOrientation) -> RuleOrientation:
|
|
if orientation not in _VALID_RULE_ORIENTATIONS:
|
|
raise InvalidRuleOrientation(
|
|
f"Valid rule orientations are {friendly_list(_VALID_RULE_ORIENTATIONS)}"
|
|
)
|
|
return orientation
|
|
|
|
def validate_line_style(self, style: LineStyle) -> LineStyle:
|
|
if style not in _VALID_LINE_STYLES:
|
|
raise InvalidLineStyle(
|
|
f"Valid rule line styles are {friendly_list(_VALID_LINE_STYLES)}"
|
|
)
|
|
return style
|
|
|
|
def get_content_width(self, container: Size, viewport: Size) -> int:
|
|
if self.orientation == "horizontal":
|
|
return container.width
|
|
return 1
|
|
|
|
def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
|
|
if self.orientation == "horizontal":
|
|
return 1
|
|
return container.height
|
|
|
|
@classmethod
|
|
def horizontal(
|
|
cls,
|
|
line_style: LineStyle = "solid",
|
|
name: str | None = None,
|
|
id: str | None = None,
|
|
classes: str | None = None,
|
|
disabled: bool = False,
|
|
) -> Rule:
|
|
"""Utility constructor for creating a horizontal rule.
|
|
|
|
Args:
|
|
line_style: The line style of the rule.
|
|
name: The name of the widget.
|
|
id: The ID of the widget in the DOM.
|
|
classes: The CSS classes of the widget.
|
|
disabled: Whether the widget is disabled or not.
|
|
|
|
Returns:
|
|
A rule widget with horizontal orientation.
|
|
"""
|
|
return Rule(
|
|
orientation="horizontal",
|
|
line_style=line_style,
|
|
name=name,
|
|
id=id,
|
|
classes=classes,
|
|
disabled=disabled,
|
|
)
|
|
|
|
@classmethod
|
|
def vertical(
|
|
cls,
|
|
line_style: LineStyle = "solid",
|
|
name: str | None = None,
|
|
id: str | None = None,
|
|
classes: str | None = None,
|
|
disabled: bool = False,
|
|
) -> Rule:
|
|
"""Utility constructor for creating a vertical rule.
|
|
|
|
Args:
|
|
line_style: The line style of the rule.
|
|
name: The name of the widget.
|
|
id: The ID of the widget in the DOM.
|
|
classes: The CSS classes of the widget.
|
|
disabled: Whether the widget is disabled or not.
|
|
|
|
Returns:
|
|
A rule widget with vertical orientation.
|
|
"""
|
|
return Rule(
|
|
orientation="vertical",
|
|
line_style=line_style,
|
|
name=name,
|
|
id=id,
|
|
classes=classes,
|
|
disabled=disabled,
|
|
)
|