mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Tidying Tabs, adding docstrings
This commit is contained in:
@@ -19,12 +19,19 @@ from textual.renderables.opacity import Opacity
|
||||
from textual.renderables.underline_bar import UnderlineBar
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
__all__ = ["Tab", "Tabs"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tab:
|
||||
"""Data container representing a single tab.
|
||||
|
||||
Attributes:
|
||||
label (str): The user-facing label that will appear inside the tab.
|
||||
name (str | None): A unique string key that will identify the tab. If None, it will default to the label.
|
||||
If the name is not unique within a single list of tabs, only the final Tab will be displayed.
|
||||
"""
|
||||
|
||||
label: str
|
||||
name: str | None = None
|
||||
|
||||
@@ -37,6 +44,8 @@ class Tab:
|
||||
|
||||
|
||||
class TabsRenderable:
|
||||
"""Renderable for the Tabs widget."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tabs: Iterable[Tab],
|
||||
@@ -76,35 +85,12 @@ class TabsRenderable:
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
if self.tabs:
|
||||
yield from self.get_tab_headers(console, options)
|
||||
yield from self.get_tab_labels(console, options)
|
||||
yield Segment.line()
|
||||
yield from self.get_underline_bar(console)
|
||||
|
||||
def get_underline_bar(self, console):
|
||||
if self.tabs:
|
||||
ranges = self._label_range_cache
|
||||
tab_index = int(self.bar_offset)
|
||||
next_tab_index = (tab_index + 1) % len(ranges)
|
||||
range_values = list(ranges.values())
|
||||
tab1_start, tab1_end = range_values[tab_index]
|
||||
tab2_start, tab2_end = range_values[next_tab_index]
|
||||
|
||||
bar_start = tab1_start + (tab2_start - tab1_start) * (
|
||||
self.bar_offset - tab_index
|
||||
)
|
||||
bar_end = tab1_end + (tab2_end - tab1_end) * (self.bar_offset - tab_index)
|
||||
else:
|
||||
bar_start = 0
|
||||
bar_end = 0
|
||||
underline = UnderlineBar(
|
||||
highlight_range=(bar_start, bar_end),
|
||||
highlight_style=self.active_bar_style,
|
||||
background_style=self.inactive_bar_style,
|
||||
clickable_ranges=self._selection_range_cache,
|
||||
)
|
||||
yield from console.render(underline)
|
||||
|
||||
def get_tab_headers(self, console, options):
|
||||
def get_tab_labels(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
||||
"""Yields the spaced-out labels that appear above the line for the Tabs widget"""
|
||||
width = self.width or options.max_width
|
||||
tab_values = self.tabs.values()
|
||||
|
||||
@@ -165,6 +151,31 @@ class TabsRenderable:
|
||||
label_cell_cursor + len_label_text + rpad,
|
||||
)
|
||||
|
||||
def get_underline_bar(self, console: Console) -> RenderResult:
|
||||
"""Yields the bar that appears below the tab labels in the Tabs widget"""
|
||||
if self.tabs:
|
||||
ranges = self._label_range_cache
|
||||
tab_index = int(self.bar_offset)
|
||||
next_tab_index = (tab_index + 1) % len(ranges)
|
||||
range_values = list(ranges.values())
|
||||
tab1_start, tab1_end = range_values[tab_index]
|
||||
tab2_start, tab2_end = range_values[next_tab_index]
|
||||
|
||||
bar_start = tab1_start + (tab2_start - tab1_start) * (
|
||||
self.bar_offset - tab_index
|
||||
)
|
||||
bar_end = tab1_end + (tab2_end - tab1_end) * (self.bar_offset - tab_index)
|
||||
else:
|
||||
bar_start = 0
|
||||
bar_end = 0
|
||||
underline = UnderlineBar(
|
||||
highlight_range=(bar_start, bar_end),
|
||||
highlight_style=self.active_bar_style,
|
||||
background_style=self.inactive_bar_style,
|
||||
clickable_ranges=self._selection_range_cache,
|
||||
)
|
||||
yield from console.render(underline)
|
||||
|
||||
|
||||
class Tabs(Widget):
|
||||
"""Widget which displays a set of horizontal tabs.
|
||||
@@ -176,11 +187,11 @@ class Tabs(Widget):
|
||||
active_bar_style (StyleType): Style to apply to the underline of the active tab.
|
||||
inactive_tab_style (StyleType): Style to apply to the label of inactive tabs.
|
||||
inactive_bar_style (StyleType): Style to apply to the underline of inactive tabs.
|
||||
inactive_text_opacity (float): Opacity of the labels of inactive tabs.
|
||||
inactive_text_opacity (float): Opacity of the text labels of inactive tabs.
|
||||
animation_duration (float): The duration of the tab change animation, in seconds.
|
||||
animation_function (str): The easing function to use for the tab change animation.
|
||||
tab_padding (int | None): The horizontal padding at the side of each tab. If None,
|
||||
tabs will automatically receive padding such that they fit available space.
|
||||
tab_padding (int | None): The padding at the side of each tab. If None, tabs will
|
||||
automatically be padded such that they fit the available horizontal space.
|
||||
search_by_first_character (bool): If True, entering a character on your keyboard
|
||||
will activate the next tab (in left-to-right order) with a label starting with
|
||||
that character.
|
||||
@@ -208,7 +219,7 @@ class Tabs(Widget):
|
||||
super().__init__()
|
||||
self.tabs = tabs
|
||||
|
||||
self._bar_offset = float(self.get_tab_index(active_tab) or 0)
|
||||
self._bar_offset = float(self.find_tab_by_name(active_tab) or 0)
|
||||
self._active_tab_name = active_tab or next(iter(self.tabs), None)
|
||||
|
||||
self.active_tab_style = active_tab_style
|
||||
@@ -226,11 +237,22 @@ class Tabs(Widget):
|
||||
self.search_by_first_character = search_by_first_character
|
||||
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
"""Handles key press events when this widget is in focus.
|
||||
Pressing "escape" removes focus from this widget. Use the left and
|
||||
right arrow keys to cycle through tabs. Use number keys to jump to tabs
|
||||
based in their number ("1" jumps to the leftmost tab). Type a character
|
||||
to cycle through tabs with labels beginning with that character.
|
||||
|
||||
Args:
|
||||
event (events.Key): The Key event being handled
|
||||
"""
|
||||
if not self.tabs:
|
||||
event.prevent_default()
|
||||
return
|
||||
|
||||
if event.key == Keys.Right:
|
||||
if event.key == Keys.Escape:
|
||||
self.app.set_focus(None)
|
||||
elif event.key == Keys.Right:
|
||||
self.activate_next_tab()
|
||||
elif event.key == Keys.Left:
|
||||
self.activate_previous_tab()
|
||||
@@ -242,18 +264,26 @@ class Tabs(Widget):
|
||||
event.prevent_default()
|
||||
|
||||
def activate_next_tab(self) -> None:
|
||||
current_tab_index = self.get_tab_index(self._active_tab_name)
|
||||
"""Activate the tab to the right of the currently active tab"""
|
||||
current_tab_index = self.find_tab_by_name(self._active_tab_name)
|
||||
next_tab_index = (current_tab_index + 1) % len(self.tabs)
|
||||
next_tab_name = self.tabs[next_tab_index].name
|
||||
self._active_tab_name = next_tab_name
|
||||
|
||||
def activate_previous_tab(self) -> None:
|
||||
current_tab_index = self.get_tab_index(self._active_tab_name)
|
||||
"""Activate the tab to the left of the currently active tab"""
|
||||
current_tab_index = self.find_tab_by_name(self._active_tab_name)
|
||||
previous_tab_index = current_tab_index - 1
|
||||
previous_tab_name = self.tabs[previous_tab_index].name
|
||||
self._active_tab_name = previous_tab_name
|
||||
|
||||
def activate_tab_by_first_char(self, char: str) -> None:
|
||||
"""Activate the next tab that begins with the character
|
||||
|
||||
Args:
|
||||
char (str): The character to search for
|
||||
"""
|
||||
|
||||
def find_next_matching_tab(
|
||||
char: str, start: int | None, end: int | None
|
||||
) -> Tab | None:
|
||||
@@ -261,7 +291,7 @@ class Tabs(Widget):
|
||||
if tab.label.lower().startswith(char.lower()):
|
||||
return tab
|
||||
|
||||
current_tab_index = self.get_tab_index(self._active_tab_name)
|
||||
current_tab_index = self.find_tab_by_name(self._active_tab_name)
|
||||
next_tab_index = (current_tab_index + 1) % len(self.tabs)
|
||||
|
||||
next_matching_tab = find_next_matching_tab(char, next_tab_index, None)
|
||||
@@ -272,6 +302,12 @@ class Tabs(Widget):
|
||||
self._active_tab_name = next_matching_tab.name
|
||||
|
||||
def activate_tab_by_number(self, tab_number: int) -> None:
|
||||
"""Activate a tab using the tab number.
|
||||
|
||||
Args:
|
||||
tab_number (int): The number of the tab.
|
||||
The leftmost tab is number 1, the next is 2, and so on. 0 represents the 10th tab.
|
||||
"""
|
||||
if tab_number > len(self.tabs):
|
||||
return
|
||||
if tab_number == 0 and len(self.tabs) >= 10:
|
||||
@@ -279,10 +315,12 @@ class Tabs(Widget):
|
||||
self._active_tab_name = self.tabs[tab_number - 1].name
|
||||
|
||||
def action_range_clicked(self, target_tab_name: str) -> None:
|
||||
"""Handles 'range_clicked' actions which are fired when tabs are clicked"""
|
||||
self._active_tab_name = target_tab_name
|
||||
|
||||
def watch__active_tab_name(self, tab_name: str) -> None:
|
||||
target_tab_index = self.get_tab_index(tab_name)
|
||||
"""Animates the underline bar position when the active tab changes"""
|
||||
target_tab_index = self.find_tab_by_name(tab_name)
|
||||
self.animate(
|
||||
"_bar_offset",
|
||||
float(target_tab_index),
|
||||
@@ -290,7 +328,12 @@ class Tabs(Widget):
|
||||
duration=self.animation_duration,
|
||||
)
|
||||
|
||||
def get_tab_index(self, tab_name: str) -> int:
|
||||
def find_tab_by_name(self, tab_name: str) -> int:
|
||||
"""Return the index of the first tab with a certain name
|
||||
|
||||
Args:
|
||||
tab_name (str): The name to search for.
|
||||
"""
|
||||
return next((i for i, tab in enumerate(self.tabs) if tab.name == tab_name), 0)
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
|
||||
Reference in New Issue
Block a user