mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Unroll tab headers into single renderable, more parameterisation of tabs, more examples
This commit is contained in:
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
from rich.console import RenderableType
|
from rich.console import RenderableType
|
||||||
from rich.padding import Padding
|
from rich.padding import Padding
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
from textual import events
|
from textual import events
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
@@ -11,14 +12,14 @@ from textual.widgets.tabs import Tabs
|
|||||||
|
|
||||||
|
|
||||||
class Info(Widget):
|
class Info(Widget):
|
||||||
def __init__(self, text: str, emoji: bool = True) -> None:
|
DEFAULT_STYLES = "height: 1;"
|
||||||
|
|
||||||
|
def __init__(self, text: str) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.text = text
|
self.text = text
|
||||||
self.emoji = emoji
|
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
prefix = "ℹ️ " if self.emoji else ""
|
return Padding(f"{self.text}", pad=(0, 1))
|
||||||
return Padding(f"{prefix}{self.text}", pad=0)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -32,27 +33,15 @@ class BasicApp(App):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.tab_keys = {
|
self.keys_to_tabs = {
|
||||||
"1": "one",
|
"1": Tab("January", name="one"),
|
||||||
"2": "two",
|
"2": Tab("に月", name="two"),
|
||||||
"3": "three",
|
"3": Tab("March", name="three"),
|
||||||
"4": "four",
|
"4": Tab("April", name="four"),
|
||||||
"5": "five",
|
"5": Tab("May", name="five"),
|
||||||
"6": "six",
|
"6": Tab("And a really long tab!", name="six"),
|
||||||
}
|
}
|
||||||
tabs = [
|
tabs = list(self.keys_to_tabs.values())
|
||||||
Tab("January", name="one"),
|
|
||||||
Tab("に月", name="two"),
|
|
||||||
Tab("March", name="three"),
|
|
||||||
Tab("April", name="four"),
|
|
||||||
Tab("May", name="five"),
|
|
||||||
Tab("And a really long tab!", name="six"),
|
|
||||||
# Tab("Four", name="five"),
|
|
||||||
# Tab("Four", name="six"),
|
|
||||||
# Tab("Four", name="seven"),
|
|
||||||
# Tab("Four", name="eight"),
|
|
||||||
# Tab("Four", name="nine"),
|
|
||||||
]
|
|
||||||
self.examples = [
|
self.examples = [
|
||||||
WidgetDescription(
|
WidgetDescription(
|
||||||
"Customise the spacing between tabs, e.g. tab_padding=1",
|
"Customise the spacing between tabs, e.g. tab_padding=1",
|
||||||
@@ -71,15 +60,6 @@ class BasicApp(App):
|
|||||||
tab_padding=2,
|
tab_padding=2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
WidgetDescription(
|
|
||||||
"Choose which tab to start on by name, e.g. active_tab='three'",
|
|
||||||
Tabs(
|
|
||||||
tabs,
|
|
||||||
active_tab="three",
|
|
||||||
active_bar_style="#FFCB4D",
|
|
||||||
tab_padding=3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
WidgetDescription(
|
WidgetDescription(
|
||||||
"Change the color of the inactive portions of the underline, e.g. inactive_bar_style='blue'",
|
"Change the color of the inactive portions of the underline, e.g. inactive_bar_style='blue'",
|
||||||
Tabs(
|
Tabs(
|
||||||
@@ -97,17 +77,38 @@ class BasicApp(App):
|
|||||||
inactive_text_opacity=1,
|
inactive_text_opacity=1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
WidgetDescription(
|
||||||
|
"Change the styling of active and inactive labels (active_tab_style, inactive_tab_style)",
|
||||||
|
Tabs(
|
||||||
|
tabs,
|
||||||
|
active_tab="one",
|
||||||
|
active_bar_style="#DA812D",
|
||||||
|
active_tab_style="bold #FFCB4D on #021720",
|
||||||
|
inactive_tab_style="italic #887AEF on #021720",
|
||||||
|
inactive_bar_style="#695CC8",
|
||||||
|
inactive_text_opacity=0.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
WidgetDescription(
|
WidgetDescription(
|
||||||
"Change the animation duration and function (animation_duration=1, animation_function='out_quad')",
|
"Change the animation duration and function (animation_duration=1, animation_function='out_quad')",
|
||||||
Tabs(
|
Tabs(
|
||||||
tabs,
|
tabs,
|
||||||
active_tab="one",
|
active_tab="one",
|
||||||
active_bar_style="#695CC8",
|
active_bar_style="#887AEF",
|
||||||
inactive_text_opacity=0.2,
|
inactive_text_opacity=0.2,
|
||||||
animation_duration=1,
|
animation_duration=1,
|
||||||
animation_function="out_quad",
|
animation_function="out_quad",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
WidgetDescription(
|
||||||
|
"Choose which tab to start on by name, e.g. active_tab='three'",
|
||||||
|
Tabs(
|
||||||
|
tabs,
|
||||||
|
active_tab="three",
|
||||||
|
active_bar_style="#FFCB4D",
|
||||||
|
tab_padding=3,
|
||||||
|
),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def on_load(self):
|
def on_load(self):
|
||||||
@@ -119,7 +120,9 @@ class BasicApp(App):
|
|||||||
|
|
||||||
def on_key(self, event: events.Key) -> None:
|
def on_key(self, event: events.Key) -> None:
|
||||||
for example in self.examples:
|
for example in self.examples:
|
||||||
example.widget.active_tab_name = self.tab_keys.get(event.key, "one")
|
tab = self.keys_to_tabs.get(event.key)
|
||||||
|
if tab:
|
||||||
|
example.widget.active_tab_name = tab.name
|
||||||
|
|
||||||
def on_mount(self):
|
def on_mount(self):
|
||||||
"""Build layout here."""
|
"""Build layout here."""
|
||||||
@@ -128,11 +131,11 @@ class BasicApp(App):
|
|||||||
"\n"
|
"\n"
|
||||||
"• The examples below show customisation options for the [#1493FF]Tabs[/] widget.\n"
|
"• The examples below show customisation options for the [#1493FF]Tabs[/] widget.\n"
|
||||||
"• Press keys 1-6 on your keyboard to switch tabs, or click on a tab.",
|
"• Press keys 1-6 on your keyboard to switch tabs, or click on a tab.",
|
||||||
emoji=False,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for example in self.examples:
|
for example in self.examples:
|
||||||
self.mount(Info(example.description))
|
info = Info(example.description)
|
||||||
|
self.mount(info)
|
||||||
self.mount(example.widget)
|
self.mount(example.widget)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
$background: #262626;
|
$background: #021720;
|
||||||
|
|
||||||
App > View {
|
App > View {
|
||||||
docks: side=left/1;
|
|
||||||
text: on $background;
|
text: on $background;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ from typing import Iterable
|
|||||||
|
|
||||||
from rich.cells import cell_len
|
from rich.cells import cell_len
|
||||||
from rich.console import Console, ConsoleOptions, RenderResult
|
from rich.console import Console, ConsoleOptions, RenderResult
|
||||||
from rich.style import Style
|
from rich.style import Style, StyleType
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
from textual.renderables.opacity import Opacity
|
from textual.renderables.opacity import Opacity
|
||||||
|
from textual.renderables.underline_bar import UnderlineBar
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -29,16 +30,22 @@ class TabHeadersRenderable:
|
|||||||
self,
|
self,
|
||||||
tabs: Iterable[Tab],
|
tabs: Iterable[Tab],
|
||||||
*,
|
*,
|
||||||
active_tab_name: str | None = None,
|
active_tab_name: str,
|
||||||
|
active_bar_style: StyleType,
|
||||||
|
inactive_bar_style: StyleType,
|
||||||
|
inactive_text_opacity: float,
|
||||||
|
tab_padding: int | None,
|
||||||
|
bar_offset: float,
|
||||||
width: int | None = None,
|
width: int | None = None,
|
||||||
tab_padding: int | None = None,
|
|
||||||
inactive_tab_opacity: float = 0.5,
|
|
||||||
):
|
):
|
||||||
self.tabs = {tab.name: tab for tab in tabs}
|
self.tabs = {tab.name: tab for tab in tabs}
|
||||||
self.active_tab_name = active_tab_name or next(iter(self.tabs))
|
self.active_tab_name = active_tab_name or next(iter(self.tabs))
|
||||||
|
self.active_bar_style = active_bar_style
|
||||||
|
self.inactive_bar_style = inactive_bar_style
|
||||||
|
self.bar_offset = bar_offset
|
||||||
self.width = width
|
self.width = width
|
||||||
self.tab_padding = tab_padding
|
self.tab_padding = tab_padding
|
||||||
self.inactive_tab_opacity = inactive_tab_opacity
|
self.inactive_text_opacity = inactive_text_opacity
|
||||||
|
|
||||||
self._range_cache: dict[str, tuple[int, int]] = {}
|
self._range_cache: dict[str, tuple[int, int]] = {}
|
||||||
|
|
||||||
@@ -86,23 +93,30 @@ class TabHeadersRenderable:
|
|||||||
yield tab_content
|
yield tab_content
|
||||||
else:
|
else:
|
||||||
dimmed_tab_content = Opacity(
|
dimmed_tab_content = Opacity(
|
||||||
tab_content, opacity=self.inactive_tab_opacity
|
tab_content, opacity=self.inactive_text_opacity
|
||||||
)
|
)
|
||||||
segments = list(console.render(dimmed_tab_content))
|
segments = list(console.render(dimmed_tab_content))
|
||||||
yield from segments
|
yield from segments
|
||||||
|
|
||||||
yield pad
|
yield pad
|
||||||
|
|
||||||
|
ranges = self.get_ranges()
|
||||||
|
tab_index = int(self.bar_offset)
|
||||||
|
next_tab_index = (tab_index + 1) % len(ranges)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
range_values = list(ranges.values())
|
||||||
console = Console()
|
|
||||||
|
|
||||||
h = TabHeadersRenderable(
|
tab1_start, tab1_end = range_values[tab_index]
|
||||||
[
|
tab2_start, tab2_end = range_values[next_tab_index]
|
||||||
Tab("One"),
|
|
||||||
Tab("Two"),
|
bar_start = tab1_start + (tab2_start - tab1_start) * (
|
||||||
Tab("Three"),
|
self.bar_offset - tab_index
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
bar_end = tab1_end + (tab2_end - tab1_end) * (self.bar_offset - tab_index)
|
||||||
|
|
||||||
console.print(h)
|
underline = UnderlineBar(
|
||||||
|
highlight_range=(bar_start, bar_end),
|
||||||
|
highlight_style=self.active_bar_style,
|
||||||
|
background_style=self.inactive_bar_style,
|
||||||
|
)
|
||||||
|
yield from console.render(underline)
|
||||||
|
|||||||
@@ -36,11 +36,9 @@ class Opacity:
|
|||||||
color_style = _get_blended_style_cached(
|
color_style = _get_blended_style_cached(
|
||||||
fg_color=fg, bg_color=bg, opacity=opacity
|
fg_color=fg, bg_color=bg, opacity=opacity
|
||||||
)
|
)
|
||||||
meta_style = Style.from_meta(style.meta)
|
|
||||||
new_style = color_style + meta_style
|
|
||||||
yield Segment(
|
yield Segment(
|
||||||
segment.text,
|
segment.text,
|
||||||
new_style,
|
style + color_style,
|
||||||
segment.control,
|
segment.control,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
from rich.console import ConsoleOptions, Console, RenderResult
|
from rich.console import ConsoleOptions, Console, RenderResult
|
||||||
from rich.segment import Segment
|
from rich.segment import Segment
|
||||||
from rich.style import StyleType
|
from rich.style import StyleType
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
|
||||||
class UnderlineBar:
|
class UnderlineBar:
|
||||||
@@ -20,11 +21,13 @@ class UnderlineBar:
|
|||||||
highlight_range: tuple[float, float] = (0, 0),
|
highlight_range: tuple[float, float] = (0, 0),
|
||||||
highlight_style: StyleType = "magenta",
|
highlight_style: StyleType = "magenta",
|
||||||
background_style: StyleType = "grey37",
|
background_style: StyleType = "grey37",
|
||||||
|
clickable_ranges: dict[str, tuple[int, int]] | None = None,
|
||||||
width: int | None = None,
|
width: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.highlight_range = highlight_range
|
self.highlight_range = highlight_range
|
||||||
self.highlight_style = highlight_style
|
self.highlight_style = highlight_style
|
||||||
self.background_style = background_style
|
self.background_style = background_style
|
||||||
|
self.clickable_ranges = clickable_ranges
|
||||||
self.width = width
|
self.width = width
|
||||||
|
|
||||||
def __rich_console__(
|
def __rich_console__(
|
||||||
@@ -102,17 +105,13 @@ if __name__ == "__main__":
|
|||||||
for range in ranges:
|
for range in ranges:
|
||||||
color = random.choice(list(ANSI_COLOR_NAMES.keys()))
|
color = random.choice(list(ANSI_COLOR_NAMES.keys()))
|
||||||
console.print(
|
console.print(
|
||||||
UnderlineBar(
|
UnderlineBar(range, highlight_style=color, width=20),
|
||||||
range,
|
|
||||||
highlight_style=color,
|
|
||||||
width=20,
|
|
||||||
),
|
|
||||||
f" {range}",
|
f" {range}",
|
||||||
)
|
)
|
||||||
|
|
||||||
from rich.live import Live
|
from rich.live import Live
|
||||||
|
|
||||||
bar = UnderlineBar(width=80, highlight_range=(0, 4.5))
|
bar = UnderlineBar(highlight_range=(0, 4.5), width=80)
|
||||||
with Live(bar, refresh_per_second=60) as live:
|
with Live(bar, refresh_per_second=60) as live:
|
||||||
while True:
|
while True:
|
||||||
bar.highlight_range = (
|
bar.highlight_range = (
|
||||||
|
|||||||
@@ -174,8 +174,8 @@ class Widget(DOMNode):
|
|||||||
style=renderable_text_style,
|
style=renderable_text_style,
|
||||||
)
|
)
|
||||||
|
|
||||||
if styles.opacity:
|
# if styles.opacity:
|
||||||
renderable = Opacity(renderable, opacity=styles.opacity)
|
# renderable = Opacity(renderable, opacity=styles.opacity)
|
||||||
|
|
||||||
return renderable
|
return renderable
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +1,90 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
from rich.cells import cell_len
|
||||||
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
||||||
from rich.segment import Segment
|
from rich.segment import Segment
|
||||||
from rich.style import StyleType
|
from rich.style import StyleType, Style
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
from textual.reactive import Reactive
|
from textual.reactive import Reactive
|
||||||
from textual.renderables._tab_headers import TabHeadersRenderable, Tab
|
from textual.renderables._tab_headers import Tab
|
||||||
|
from textual.renderables.opacity import Opacity
|
||||||
from textual.renderables.underline_bar import UnderlineBar
|
from textual.renderables.underline_bar import UnderlineBar
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Tab:
|
||||||
|
label: str
|
||||||
|
name: str | None = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.name is None:
|
||||||
|
self.name = self.label
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.label
|
||||||
|
|
||||||
|
|
||||||
class TabsRenderable:
|
class TabsRenderable:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
tabs: list[Tab],
|
tabs: Iterable[Tab],
|
||||||
|
*,
|
||||||
active_tab_name: str,
|
active_tab_name: str,
|
||||||
|
active_tab_style: StyleType,
|
||||||
active_bar_style: StyleType,
|
active_bar_style: StyleType,
|
||||||
|
inactive_tab_style: StyleType,
|
||||||
inactive_bar_style: StyleType,
|
inactive_bar_style: StyleType,
|
||||||
inactive_text_opacity: float,
|
inactive_text_opacity: float,
|
||||||
tab_padding: int | None,
|
tab_padding: int | None,
|
||||||
bar_offset: float,
|
bar_offset: float,
|
||||||
|
width: int | None = None,
|
||||||
):
|
):
|
||||||
self.tabs = tabs
|
self.tabs = {tab.name: tab for tab in tabs}
|
||||||
self.active_tab_name = active_tab_name
|
|
||||||
|
try:
|
||||||
|
self.active_tab_name = active_tab_name or next(iter(self.tabs))
|
||||||
|
except StopIteration:
|
||||||
|
self.active_tab_name = None
|
||||||
|
|
||||||
|
self.active_tab_style = active_tab_style
|
||||||
self.active_bar_style = active_bar_style
|
self.active_bar_style = active_bar_style
|
||||||
|
|
||||||
|
self.inactive_tab_style = inactive_tab_style
|
||||||
self.inactive_bar_style = inactive_bar_style
|
self.inactive_bar_style = inactive_bar_style
|
||||||
self.inactive_text_opacity = inactive_text_opacity
|
|
||||||
self.tab_padding = tab_padding
|
|
||||||
self.bar_offset = bar_offset
|
self.bar_offset = bar_offset
|
||||||
|
self.width = width
|
||||||
|
self.tab_padding = tab_padding
|
||||||
|
self.inactive_text_opacity = inactive_text_opacity
|
||||||
|
|
||||||
|
self._label_range_cache: dict[str, tuple[int, int]] = {}
|
||||||
|
self._selection_range_cache: dict[str, tuple[int, int]] = {}
|
||||||
|
|
||||||
def __rich_console__(
|
def __rich_console__(
|
||||||
self, console: Console, options: ConsoleOptions
|
self, console: Console, options: ConsoleOptions
|
||||||
) -> RenderResult:
|
) -> RenderResult:
|
||||||
headers = TabHeadersRenderable(
|
if self.tabs:
|
||||||
self.tabs,
|
yield from self.get_tab_headers(console, options)
|
||||||
active_tab_name=self.active_tab_name,
|
|
||||||
tab_padding=self.tab_padding,
|
|
||||||
inactive_tab_opacity=self.inactive_text_opacity,
|
|
||||||
)
|
|
||||||
yield from console.render(headers)
|
|
||||||
yield Segment.line()
|
yield Segment.line()
|
||||||
|
yield from self.get_underline_bar(console)
|
||||||
|
|
||||||
ranges = headers.get_ranges()
|
def get_active_range(self) -> tuple[int, int]:
|
||||||
|
return self._label_range_cache[self.active_tab_name]
|
||||||
|
|
||||||
|
def get_ranges(self):
|
||||||
|
return self._label_range_cache
|
||||||
|
|
||||||
|
def get_underline_bar(self, console):
|
||||||
|
if self.tabs:
|
||||||
|
ranges = self.get_ranges()
|
||||||
tab_index = int(self.bar_offset)
|
tab_index = int(self.bar_offset)
|
||||||
next_tab_index = (tab_index + 1) % len(ranges)
|
next_tab_index = (tab_index + 1) % len(ranges)
|
||||||
|
|
||||||
range_values = list(ranges.values())
|
range_values = list(ranges.values())
|
||||||
|
|
||||||
tab1_start, tab1_end = range_values[tab_index]
|
tab1_start, tab1_end = range_values[tab_index]
|
||||||
tab2_start, tab2_end = range_values[next_tab_index]
|
tab2_start, tab2_end = range_values[next_tab_index]
|
||||||
|
|
||||||
@@ -54,26 +92,78 @@ class TabsRenderable:
|
|||||||
self.bar_offset - tab_index
|
self.bar_offset - tab_index
|
||||||
)
|
)
|
||||||
bar_end = tab1_end + (tab2_end - tab1_end) * (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(
|
underline = UnderlineBar(
|
||||||
highlight_range=(bar_start, bar_end),
|
highlight_range=(bar_start, bar_end),
|
||||||
highlight_style=self.active_bar_style,
|
highlight_style=self.active_bar_style,
|
||||||
background_style=self.inactive_bar_style,
|
background_style=self.inactive_bar_style,
|
||||||
|
clickable_ranges=self._selection_range_cache,
|
||||||
)
|
)
|
||||||
yield from console.render(underline)
|
yield from console.render(underline)
|
||||||
|
|
||||||
|
def get_tab_headers(self, console, options):
|
||||||
|
width = self.width or options.max_width
|
||||||
|
tabs = self.tabs
|
||||||
|
tab_values = self.tabs.values()
|
||||||
|
if self.tab_padding is None:
|
||||||
|
total_len = sum(cell_len(header.label) for header in tab_values)
|
||||||
|
free_space = width - total_len
|
||||||
|
label_pad = (free_space // len(tabs) + 1) // 2
|
||||||
|
else:
|
||||||
|
label_pad = self.tab_padding
|
||||||
|
pad = Text(" " * label_pad, end="")
|
||||||
|
char_index = label_pad
|
||||||
|
active_tab_style = console.get_style(self.active_tab_style)
|
||||||
|
inactive_tab_style = console.get_style(self.inactive_tab_style)
|
||||||
|
for tab_index, tab in enumerate(tab_values):
|
||||||
|
# Cache and move to next label
|
||||||
|
len_label = cell_len(tab.label)
|
||||||
|
self._label_range_cache[tab.name] = (char_index, char_index + len_label)
|
||||||
|
self._selection_range_cache[tab.name] = (
|
||||||
|
char_index - label_pad,
|
||||||
|
char_index + len_label + label_pad,
|
||||||
|
)
|
||||||
|
|
||||||
|
char_index += len_label + label_pad * 2
|
||||||
|
|
||||||
|
if tab.name == self.active_tab_name:
|
||||||
|
tab_content = Text(
|
||||||
|
f"{pad}{tab.label}{pad}",
|
||||||
|
end="",
|
||||||
|
style=active_tab_style,
|
||||||
|
)
|
||||||
|
yield tab_content
|
||||||
|
else:
|
||||||
|
tab_content = Text(
|
||||||
|
f"{pad}{tab.label}{pad}",
|
||||||
|
end="",
|
||||||
|
style=inactive_tab_style
|
||||||
|
+ Style.from_meta({"@click": f"activate_tab('{tab.name}')"}),
|
||||||
|
)
|
||||||
|
dimmed_tab_content = Opacity(
|
||||||
|
tab_content, opacity=self.inactive_text_opacity
|
||||||
|
)
|
||||||
|
segments = list(console.render(dimmed_tab_content))
|
||||||
|
yield from segments
|
||||||
|
|
||||||
|
|
||||||
class Tabs(Widget):
|
class Tabs(Widget):
|
||||||
"""Horizontal tabs"""
|
"""Horizontal tabs"""
|
||||||
|
|
||||||
active_tab_name: Reactive[str] = Reactive("")
|
DEFAULT_STYLES = "height: 2;"
|
||||||
|
|
||||||
|
active_tab_name: Reactive[str | None] = Reactive("")
|
||||||
bar_offset: Reactive[float] = Reactive(0.0)
|
bar_offset: Reactive[float] = Reactive(0.0)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
tabs: list[Tab],
|
tabs: list[Tab],
|
||||||
active_tab: str | None = None,
|
active_tab: str | None = None,
|
||||||
|
active_tab_style: StyleType = "#f0f0f0 on #021720",
|
||||||
active_bar_style: StyleType = "#1BB152",
|
active_bar_style: StyleType = "#1BB152",
|
||||||
|
inactive_tab_style: StyleType = "#f0f0f0 on #021720",
|
||||||
inactive_bar_style: StyleType = "#455058",
|
inactive_bar_style: StyleType = "#455058",
|
||||||
inactive_text_opacity: float = 0.5,
|
inactive_text_opacity: float = 0.5,
|
||||||
animation_duration: float = 0.3,
|
animation_duration: float = 0.3,
|
||||||
@@ -83,10 +173,12 @@ class Tabs(Widget):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.tabs = tabs
|
self.tabs = tabs
|
||||||
|
|
||||||
# TODO: Handle empty tabs
|
self.active_tab_name = active_tab or next(iter(self.tabs), None)
|
||||||
self.active_tab_name = active_tab or tabs[0]
|
self.active_tab_style = active_tab_style
|
||||||
self.active_bar_style = active_bar_style
|
self.active_bar_style = active_bar_style
|
||||||
|
|
||||||
self.inactive_bar_style = inactive_bar_style
|
self.inactive_bar_style = inactive_bar_style
|
||||||
|
self.inactive_tab_style = inactive_tab_style
|
||||||
self.inactive_text_opacity = inactive_text_opacity
|
self.inactive_text_opacity = inactive_text_opacity
|
||||||
|
|
||||||
self.bar_offset = float(self.get_tab_index(active_tab) or 0)
|
self.bar_offset = float(self.get_tab_index(active_tab) or 0)
|
||||||
@@ -121,7 +213,9 @@ class Tabs(Widget):
|
|||||||
self.tabs,
|
self.tabs,
|
||||||
tab_padding=self.tab_padding,
|
tab_padding=self.tab_padding,
|
||||||
active_tab_name=self.active_tab_name,
|
active_tab_name=self.active_tab_name,
|
||||||
|
active_tab_style=self.active_tab_style,
|
||||||
active_bar_style=self.active_bar_style,
|
active_bar_style=self.active_bar_style,
|
||||||
|
inactive_tab_style=self.inactive_tab_style,
|
||||||
inactive_bar_style=self.inactive_bar_style,
|
inactive_bar_style=self.inactive_bar_style,
|
||||||
bar_offset=self.bar_offset,
|
bar_offset=self.bar_offset,
|
||||||
inactive_text_opacity=self.inactive_text_opacity,
|
inactive_text_opacity=self.inactive_text_opacity,
|
||||||
|
|||||||
@@ -111,12 +111,7 @@ def test_highlight_full_range_out_of_bounds_start():
|
|||||||
|
|
||||||
|
|
||||||
def test_custom_styles():
|
def test_custom_styles():
|
||||||
bar = UnderlineBar(
|
bar = UnderlineBar(highlight_range=(2, 4), highlight_style="red", background_style="green", width=6)
|
||||||
highlight_range=(2, 4),
|
|
||||||
highlight_style="red",
|
|
||||||
background_style="green",
|
|
||||||
width=6
|
|
||||||
)
|
|
||||||
assert render(bar) == (
|
assert render(bar) == (
|
||||||
f"{GREEN}━{STOP}"
|
f"{GREEN}━{STOP}"
|
||||||
f"{GREEN}╸{STOP}"
|
f"{GREEN}╸{STOP}"
|
||||||
|
|||||||
Reference in New Issue
Block a user