Tidying Tabs, adding docstrings

This commit is contained in:
Darren Burns
2022-02-18 13:41:06 +00:00
parent 0e768380da
commit 18a3fb4b57

View File

@@ -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: