mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Displaying tabs with underline
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from rich.console import RenderableType
|
||||
from rich.panel import Panel
|
||||
|
||||
from textual import events
|
||||
from textual.app import App
|
||||
from textual.renderables._tab_headers import Tab
|
||||
from textual.widget import Widget
|
||||
@@ -22,19 +23,36 @@ class BasicApp(App):
|
||||
self.bind("c", "toggle_class('#content', '-content-visible')")
|
||||
self.bind("d", "toggle_class('#footer', 'dim')")
|
||||
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
tab_keys = {
|
||||
"1": "one",
|
||||
"2": "two",
|
||||
"3": "three",
|
||||
"4": "four",
|
||||
"5": "five",
|
||||
"6": "six",
|
||||
"7": "seven",
|
||||
"8": "eight",
|
||||
}
|
||||
self.tabs.active_tab_name = tab_keys.get(event.key, "one")
|
||||
|
||||
def on_mount(self):
|
||||
self.tabs = Tabs(
|
||||
[
|
||||
Tab("One", name="one"),
|
||||
Tab("Two", name="two"),
|
||||
Tab("Three", name="three"),
|
||||
Tab("Four", name="four"),
|
||||
Tab("Five", name="five"),
|
||||
Tab("Six", name="six"),
|
||||
Tab("Seven", name="seven"),
|
||||
Tab("Eight", name="eight"),
|
||||
],
|
||||
)
|
||||
self.tabs.active_tab_name = "one"
|
||||
"""Build layout here."""
|
||||
self.mount(
|
||||
header=Tabs(
|
||||
[
|
||||
Tab("One", active=True),
|
||||
Tab("Two"),
|
||||
Tab("Three"),
|
||||
Tab("Four"),
|
||||
Tab("Five"),
|
||||
Tab("Six"),
|
||||
]
|
||||
),
|
||||
header=self.tabs,
|
||||
content=PanelWidget(),
|
||||
footer=Widget(),
|
||||
sidebar=Widget(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable
|
||||
|
||||
from rich.cells import cell_len
|
||||
from rich.console import Console, ConsoleOptions, RenderResult
|
||||
@@ -9,66 +10,89 @@ from rich.text import Text
|
||||
|
||||
from textual import log
|
||||
from textual._loop import loop_first
|
||||
from textual.renderables.opacity import Opacity
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tab:
|
||||
title: str
|
||||
active: bool = False
|
||||
key: str | None = None
|
||||
label: str
|
||||
name: str | None = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.key is None:
|
||||
self.key = self.title
|
||||
if self.name is None:
|
||||
self.name = self.label
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
return self.label
|
||||
|
||||
|
||||
class TabHeadersRenderable:
|
||||
def __init__(
|
||||
self,
|
||||
tabs: list[Tab],
|
||||
tabs: Iterable[Tab],
|
||||
*,
|
||||
active_tab_name: str | None = None,
|
||||
width: int | None = None,
|
||||
tab_padding: int = 1,
|
||||
):
|
||||
self.tabs = tabs
|
||||
self.tabs = {tab.name: tab for tab in tabs}
|
||||
self.active_tab_name = active_tab_name or next(iter(self.tabs))
|
||||
self.width = width
|
||||
self.tab_padding = tab_padding
|
||||
|
||||
def action_highlight(self):
|
||||
log("highlighted!")
|
||||
self._range_cache: dict[str, tuple[int, int]] = {}
|
||||
|
||||
def get_active_range(self) -> tuple[int, int]:
|
||||
return self._range_cache[self.active_tab_name]
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
width = self.width or options.max_width
|
||||
tabs = self.tabs
|
||||
tab_values = self.tabs.values()
|
||||
|
||||
padding_len = self.tab_padding * len(tabs)
|
||||
total_len = sum(cell_len(header.title) for header in tabs) + padding_len
|
||||
# There's padding at each side of a label
|
||||
padding_len = 2 * self.tab_padding * len(tabs)
|
||||
|
||||
# The total length of the labels, including their padding
|
||||
total_len = sum(cell_len(header.label) for header in tab_values) + padding_len
|
||||
|
||||
# The amount of space left to distribute around tabs
|
||||
free_space = width - total_len
|
||||
|
||||
# The gap between each tab (not including padding)
|
||||
space_per_gap = free_space // (len(tabs) + 1)
|
||||
|
||||
gap = Text(" " * space_per_gap, end="")
|
||||
lpad = rpad = Text(" " * self.tab_padding, end="")
|
||||
|
||||
for is_first, tab in loop_first(tabs):
|
||||
char_index = space_per_gap + self.tab_padding
|
||||
for tab_index, (is_first, tab) in enumerate(loop_first(tab_values)):
|
||||
if is_first:
|
||||
yield gap
|
||||
yield lpad
|
||||
|
||||
tab_content = Text(
|
||||
tab.title, end="", style=Style(meta={"@click": "highlight"})
|
||||
tab.label,
|
||||
end="",
|
||||
style=Style(
|
||||
color="#f0f0f0", bgcolor="#021720", meta={"@click": "highlight"}
|
||||
),
|
||||
)
|
||||
yield tab_content
|
||||
# if tab.active:
|
||||
# yield tab_content
|
||||
# else:
|
||||
# dimmed_tab_content = Opacity(tab_content, opacity=.2)
|
||||
# yield from console.render(dimmed_tab_content)
|
||||
|
||||
# Cache and move to next label
|
||||
len_label = len(tab.label)
|
||||
self._range_cache[tab.name] = (char_index, char_index + len_label)
|
||||
char_index += len_label + space_per_gap + self.tab_padding * 2
|
||||
|
||||
if tab.name == self.active_tab_name:
|
||||
yield tab_content
|
||||
else:
|
||||
dimmed_tab_content = tab_content
|
||||
segments = list(console.render(dimmed_tab_content))
|
||||
log(segments)
|
||||
yield from segments
|
||||
|
||||
yield rpad
|
||||
yield gap
|
||||
|
||||
|
||||
@@ -33,11 +33,14 @@ class Opacity:
|
||||
fg = style.color
|
||||
bg = style.bgcolor
|
||||
if fg and fg.triplet and bg and bg.triplet:
|
||||
color_style = _get_blended_style_cached(
|
||||
fg_color=fg, bg_color=bg, opacity=opacity
|
||||
)
|
||||
meta_style = Style.from_meta(style.meta)
|
||||
new_style = color_style + meta_style
|
||||
yield Segment(
|
||||
segment.text,
|
||||
_get_blended_style_cached(
|
||||
fg_color=fg, bg_color=bg, opacity=opacity
|
||||
),
|
||||
new_style,
|
||||
segment.control,
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -3,33 +3,38 @@ from __future__ import annotations
|
||||
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
||||
from rich.segment import Segment
|
||||
|
||||
from textual import log
|
||||
from textual import log, events
|
||||
from textual.reactive import Reactive
|
||||
from textual.renderables._tab_headers import TabHeadersRenderable, Tab
|
||||
from textual.renderables.underline_bar import UnderlineBar
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
class TabsRenderable:
|
||||
def __init__(self, headers: list[Tab]):
|
||||
self.headers = headers
|
||||
def __init__(self, tabs: list[Tab], active_tab_name: str):
|
||||
self.tabs = tabs
|
||||
self.active_tab_name = active_tab_name
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
headers = TabHeadersRenderable(tabs=self.headers)
|
||||
underline = UnderlineBar(highlight_range=(19, 26), highlight_style="#95d52a")
|
||||
headers = TabHeadersRenderable(self.tabs, active_tab_name=self.active_tab_name)
|
||||
yield from console.render(headers)
|
||||
yield Segment.line()
|
||||
# TODO: How do we choose highlight_style?
|
||||
highlight_range = headers.get_active_range()
|
||||
underline = UnderlineBar(
|
||||
highlight_range=highlight_range, highlight_style="#95d52a"
|
||||
)
|
||||
yield from console.render(underline)
|
||||
|
||||
|
||||
class Tabs(Widget):
|
||||
def __init__(self, headers: list[Tab]):
|
||||
self.headers = headers
|
||||
def __init__(self, tabs: list[Tab]):
|
||||
self.tabs = tabs
|
||||
super().__init__()
|
||||
|
||||
def action_highlight(self, header: str):
|
||||
log(f"action_header_clicked {header}")
|
||||
active_tab_name = Reactive("")
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
return TabsRenderable(self.headers)
|
||||
return TabsRenderable(self.tabs, active_tab_name=self.active_tab_name)
|
||||
|
||||
Reference in New Issue
Block a user