minecraftd/venv/lib/python3.11/site-packages/rich_toolkit/styles/border.py

261 lines
8.1 KiB
Python

from typing import Any, Optional, Tuple, Union
from rich import box
from rich.color import Color
from rich.console import Group, RenderableType
from rich.style import Style
from rich.text import Text
from rich_toolkit._rich_components import Panel
from rich_toolkit.container import Container
from rich_toolkit.element import CursorOffset, Element
from rich_toolkit.form import Form
from rich_toolkit.input import Input
from rich_toolkit.menu import Menu
from rich_toolkit.progress import Progress
from .base import BaseStyle
class BorderedStyle(BaseStyle):
box = box.SQUARE
def empty_line(self) -> RenderableType:
return ""
def _box(
self,
content: RenderableType,
title: Union[str, Text, None],
is_active: bool,
border_color: Color,
after: Tuple[str, ...] = (),
) -> RenderableType:
return Group(
Panel(
content,
title=title,
title_align="left",
highlight=is_active,
width=50,
box=self.box,
border_style=Style(color=border_color),
),
*after,
)
def render_container(
self,
element: Container,
is_active: bool = False,
done: bool = False,
parent: Optional[Element] = None,
) -> RenderableType:
content = super().render_container(element, is_active, done, parent)
if isinstance(element, Form):
return self._box(content, element.title, is_active, Color.parse("white"))
return content
def render_input(
self,
element: Input,
is_active: bool = False,
done: bool = False,
parent: Optional[Element] = None,
**metadata: Any,
) -> RenderableType:
validation_message: Tuple[str, ...] = ()
if isinstance(parent, Form):
return super().render_input(element, is_active, done, parent, **metadata)
if message := self.render_validation_message(element):
validation_message = (message,)
title = self.render_input_label(
element,
is_active=is_active,
parent=parent,
)
# Determine border color based on validation state
if element.valid is False:
try:
border_color = self.console.get_style("error").color or Color.parse(
"red"
)
except Exception:
# Fallback if error style is not defined
border_color = Color.parse("red")
else:
border_color = Color.parse("white")
return self._box(
self.render_input_value(element, is_active=is_active, parent=parent),
title,
is_active,
border_color,
after=validation_message,
)
def render_menu(
self,
element: Menu,
is_active: bool = False,
done: bool = False,
parent: Optional[Element] = None,
**metadata: Any,
) -> RenderableType:
validation_message: Tuple[str, ...] = ()
menu = Text(justify="left")
selected_prefix = Text(element.current_selection_char + " ")
not_selected_prefix = Text(element.selection_char + " ")
separator = Text("\t" if element.inline else "\n")
content: list[RenderableType] = []
if done:
content.append(
Text(
element.options[element.selected]["name"],
style=self.console.get_style("result"),
)
)
else:
# Get visible range for scrolling
all_options = element.options
start, end = element.visible_options_range
visible_options = all_options[start:end]
# Check if scrolling is needed (to reserve consistent space for indicators)
needs_scrolling = element._needs_scrolling()
# Always reserve space for "more above" indicator when scrolling is enabled
if needs_scrolling:
if element.has_more_above:
menu.append(Text(element.MORE_ABOVE_INDICATOR + "\n", style="dim"))
else:
menu.append(Text(" " * len(element.MORE_ABOVE_INDICATOR) + "\n"))
for idx, option in enumerate(visible_options):
actual_idx = start + idx
if actual_idx == element.selected:
prefix = selected_prefix
style = self.console.get_style("selected")
else:
prefix = not_selected_prefix
style = self.console.get_style("text")
is_last = idx == len(visible_options) - 1
menu.append(
Text.assemble(
prefix,
option["name"],
separator if not is_last else "",
style=style,
)
)
# Always reserve space for "more below" indicator when scrolling is enabled
if needs_scrolling:
if element.has_more_below:
menu.append(Text("\n" + element.MORE_BELOW_INDICATOR, style="dim"))
else:
menu.append(Text("\n" + " " * len(element.MORE_BELOW_INDICATOR)))
if not element.options:
menu = Text("No results found", style=self.console.get_style("text"))
filter = (
[
Text.assemble(
(element.filter_prompt, self.console.get_style("text")),
(element.text, self.console.get_style("text")),
"\n",
)
]
if element.allow_filtering
else []
)
content.extend(filter)
content.append(menu)
if message := self.render_validation_message(element):
validation_message = (message,)
result = Group(*content)
return self._box(
result,
self.render_input_label(element),
is_active,
Color.parse("white"),
after=validation_message,
)
def render_progress(
self,
element: Progress,
is_active: bool = False,
done: bool = False,
parent: Optional[Element] = None,
) -> RenderableType:
content: str | Group | Text = element.current_message
title: Union[str, Text, None] = None
title = element.title
if element.logs and element._inline_logs:
lines_to_show = (
element.logs[-element.lines_to_show :]
if element.lines_to_show > 0
else element.logs
)
content = Group(
*[
self.render_element(
line,
index=index,
max_lines=element.lines_to_show,
total_lines=len(element.logs),
)
for index, line in enumerate(lines_to_show)
]
)
border_color = Color.parse("white")
if not done:
colors = self._get_animation_colors(
steps=10, animation_status="started", breathe=True
)
border_color = colors[self.animation_counter % 10]
return self._box(content, title, is_active, border_color=border_color)
def get_cursor_offset_for_element(
self, element: Element, parent: Optional[Element] = None
) -> CursorOffset:
top_offset = element.cursor_offset.top
left_offset = element.cursor_offset.left + 2
if isinstance(element, Input) and element.inline:
# we don't support inline inputs yet in border style
top_offset += 1
inline_left_offset = (len(element.label) - 1) if element.label else 0
left_offset = element.cursor_offset.left - inline_left_offset
if isinstance(parent, Form):
top_offset += 1
return CursorOffset(top=top_offset, left=left_offset)