Displaying tabs with underline

This commit is contained in:
Darren Burns
2022-02-15 14:09:11 +00:00
parent c30d1b9795
commit b2f7c2ac85
4 changed files with 94 additions and 44 deletions

View File

@@ -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(),

View File

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

View File

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

View File

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