mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
links
This commit is contained in:
@@ -94,7 +94,7 @@ This app will "play" fizz buzz by displaying a table of the first 15 numbers and
|
||||
|
||||
=== "fizzbuzz.css"
|
||||
|
||||
```sass title="hello03.css" hl_lines="32-35"
|
||||
```sass title="fizzbuzz.css" hl_lines="32-35"
|
||||
--8<-- "docs/examples/guide/widgets/fizzbuzz.css"
|
||||
```
|
||||
|
||||
@@ -103,18 +103,49 @@ This app will "play" fizz buzz by displaying a table of the first 15 numbers and
|
||||
```{.textual path="docs/examples/guide/widgets/fizzbuzz.py"}
|
||||
```
|
||||
|
||||
|
||||
## Default CSS
|
||||
|
||||
When building an app it is best to keep all your CSS in an external file. This allows you to see all your CSS in one place, and to enable live editing. However if you are building Textual widgets in an external library it can be convenient to bundle code and CSS within the widget itself. You can do this by adding a `DEFAULT_CSS` class variable inside your widget class.
|
||||
|
||||
Textual's builtin widgets bundle CSS in this way, which is why you can see nicely styled widgets without having to cut and paste code in to your CSS file.
|
||||
|
||||
Here's the Hello example again, this time the widget has embedded default CSS:
|
||||
|
||||
=== "hello04.py"
|
||||
|
||||
```python title="hello04.py" hl_lines="8-18"
|
||||
--8<-- "docs/examples/guide/widgets/hello04.py"
|
||||
```
|
||||
|
||||
=== "hello04.css"
|
||||
|
||||
```sass title="hello04.css"
|
||||
--8<-- "docs/examples/guide/widgets/hello04.css"
|
||||
```
|
||||
|
||||
=== "Output"
|
||||
|
||||
```{.textual path="docs/examples/guide/widgets/hello04.py"}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Default specificity
|
||||
|
||||
CSS defined within `DEFAULT_CSS` has an automatically lower [specificity](./CSS.md#specificity) than CSS read from either the App's `CSS` class variable or an external stylesheet. In practice this means that your app's CSS will take precedence over any CSS bundled with widgets.
|
||||
|
||||
## Content size
|
||||
|
||||
If you use a rich renderable as content, Textual can auto-detect the dimensions of the output which will become the content area of the widget.
|
||||
|
||||
## Compound widgets
|
||||
|
||||
|
||||
## Line API
|
||||
|
||||
TODO: Widgets docs
|
||||
|
||||
- What is a widget
|
||||
- Defining a basic widget
|
||||
- Base classes Widget or Static
|
||||
- Text widgets
|
||||
- Rich renderable widgets
|
||||
- Complete widget
|
||||
- Render line widget API
|
||||
- Content size
|
||||
- Compound widgets
|
||||
- Line API
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
transition: color 300ms linear, background 300ms linear;
|
||||
}
|
||||
|
||||
Tweet.tall {
|
||||
height: 24;
|
||||
}
|
||||
|
||||
*:hover {
|
||||
/* tint: 30% red;
|
||||
@@ -47,8 +50,8 @@ DataTable {
|
||||
/*border:heavy red;*/
|
||||
/* tint: 10% green; */
|
||||
/* text-opacity: 50%; */
|
||||
background: $surface;
|
||||
padding: 1 2;
|
||||
|
||||
|
||||
margin: 1 2;
|
||||
height: 24;
|
||||
}
|
||||
@@ -118,6 +121,7 @@ Tweet {
|
||||
.scrollable {
|
||||
overflow-x: auto;
|
||||
overflow-y: scroll;
|
||||
padding: 0 2;
|
||||
margin: 1 2;
|
||||
height: 24;
|
||||
align-horizontal: center;
|
||||
@@ -125,8 +129,7 @@ Tweet {
|
||||
}
|
||||
|
||||
.code {
|
||||
height: auto;
|
||||
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -124,17 +124,18 @@ class BasicApp(App):
|
||||
|
||||
yield Vertical(
|
||||
Tweet(TweetBody()),
|
||||
Container(
|
||||
Tweet(
|
||||
Static(
|
||||
Syntax(
|
||||
CODE,
|
||||
"python",
|
||||
theme="ansi_dark",
|
||||
line_numbers=True,
|
||||
indent_guides=True,
|
||||
),
|
||||
classes="code",
|
||||
),
|
||||
classes="scrollable",
|
||||
classes="tall",
|
||||
),
|
||||
Container(table, id="table-container"),
|
||||
Container(DirectoryTree("~/"), id="tree-container"),
|
||||
|
||||
@@ -14,7 +14,8 @@ without having to render the entire screen.
|
||||
from __future__ import annotations
|
||||
|
||||
from itertools import chain
|
||||
from operator import itemgetter
|
||||
from functools import reduce
|
||||
from operator import itemgetter, __or__
|
||||
import sys
|
||||
from typing import Callable, cast, Iterator, Iterable, NamedTuple, TYPE_CHECKING
|
||||
|
||||
@@ -71,13 +72,33 @@ class MapGeometry(NamedTuple):
|
||||
CompositorMap: TypeAlias = "dict[Widget, MapGeometry]"
|
||||
|
||||
|
||||
def style_links(
|
||||
segments: Iterable[Segment], link_map: dict[str, Style]
|
||||
) -> Iterable[Segment]:
|
||||
return segments
|
||||
if not link_map:
|
||||
return segments
|
||||
|
||||
_Segment = Segment
|
||||
link_map_get = link_map.get
|
||||
|
||||
segments = [
|
||||
_Segment(text, link_map_get(style.link_id, style) if style else None, control)
|
||||
for text, style, control in segments
|
||||
]
|
||||
return segments
|
||||
|
||||
|
||||
@rich.repr.auto(angular=True)
|
||||
class LayoutUpdate:
|
||||
"""A renderable containing the result of a render for a given region."""
|
||||
|
||||
def __init__(self, lines: Lines, region: Region) -> None:
|
||||
def __init__(
|
||||
self, lines: Lines, region: Region, link_map: dict[str, Style]
|
||||
) -> None:
|
||||
self.lines = lines
|
||||
self.region = region
|
||||
self.link_map = link_map
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
@@ -87,7 +108,7 @@ class LayoutUpdate:
|
||||
move_to = Control.move_to
|
||||
for last, (y, line) in loop_last(enumerate(self.lines, self.region.y)):
|
||||
yield move_to(x, y)
|
||||
yield from line
|
||||
yield from style_links(line, self.link_map)
|
||||
if not last:
|
||||
yield new_line
|
||||
|
||||
@@ -104,6 +125,7 @@ class ChopsUpdate:
|
||||
chops: list[dict[int, list[Segment] | None]],
|
||||
spans: list[tuple[int, int, int]],
|
||||
chop_ends: list[list[int]],
|
||||
link_map: dict[str, Style],
|
||||
) -> None:
|
||||
"""A renderable which updates chops (fragments of lines).
|
||||
|
||||
@@ -115,6 +137,7 @@ class ChopsUpdate:
|
||||
self.chops = chops
|
||||
self.spans = spans
|
||||
self.chop_ends = chop_ends
|
||||
self.link_map = link_map
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
@@ -126,6 +149,7 @@ class ChopsUpdate:
|
||||
last_y = self.spans[-1][0]
|
||||
|
||||
_cell_len = cell_len
|
||||
link_map = self.link_map
|
||||
|
||||
for y, x1, x2 in self.spans:
|
||||
line = chops[y]
|
||||
@@ -135,6 +159,8 @@ class ChopsUpdate:
|
||||
if segments is None:
|
||||
continue
|
||||
|
||||
segments = style_links(segments, link_map)
|
||||
|
||||
if x > x2 or end <= x1:
|
||||
continue
|
||||
|
||||
@@ -203,6 +229,23 @@ class Compositor:
|
||||
# Regions that require an update
|
||||
self._dirty_regions: set[Region] = set()
|
||||
|
||||
self._link_map: dict[str, Style] | None = None
|
||||
|
||||
@property
|
||||
def link_map(self) -> dict[str, Style]:
|
||||
"""A mapping of link ids on to styles."""
|
||||
if self._link_map is None:
|
||||
self._link_map = cast(
|
||||
"dict[str,Style]",
|
||||
reduce(
|
||||
__or__,
|
||||
(widget._link_styles for widget in self.map.keys()),
|
||||
{},
|
||||
),
|
||||
)
|
||||
|
||||
return self._link_map
|
||||
|
||||
@classmethod
|
||||
def _regions_to_spans(
|
||||
cls, regions: Iterable[Region]
|
||||
@@ -257,6 +300,7 @@ class Compositor:
|
||||
"""
|
||||
self._cuts = None
|
||||
self._layers = None
|
||||
self._link_map = None
|
||||
self.root = parent
|
||||
self.size = size
|
||||
|
||||
@@ -744,10 +788,10 @@ class Compositor:
|
||||
|
||||
if full:
|
||||
render_lines = self._assemble_chops(chops)
|
||||
return LayoutUpdate(render_lines, screen_region)
|
||||
return LayoutUpdate(render_lines, screen_region, self.link_map)
|
||||
else:
|
||||
chop_ends = [cut_set[1:] for cut_set in cuts]
|
||||
return ChopsUpdate(chops, spans, chop_ends)
|
||||
return ChopsUpdate(chops, spans, chop_ends, self.link_map)
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
@@ -775,3 +819,4 @@ class Compositor:
|
||||
add_region(update_region)
|
||||
|
||||
self._dirty_regions.update(regions)
|
||||
self._link_map = None
|
||||
|
||||
@@ -1314,7 +1314,8 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
def bell(self) -> None:
|
||||
"""Play the console 'bell'."""
|
||||
self.console.bell()
|
||||
if not self.is_headless:
|
||||
self.console.bell()
|
||||
|
||||
async def press(self, key: str) -> bool:
|
||||
"""Handle a key press.
|
||||
|
||||
@@ -184,6 +184,12 @@ class Color(NamedTuple):
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def inverse(self) -> Color:
|
||||
"""The inverse of this color."""
|
||||
r, g, b, a = self
|
||||
return Color(255 - r, 255 - g, 255 - b, a)
|
||||
|
||||
@property
|
||||
def is_transparent(self) -> bool:
|
||||
"""Check if the color is transparent, i.e. has 0 alpha.
|
||||
|
||||
@@ -616,6 +616,11 @@ class StylesBuilder:
|
||||
process_scrollbar_background_hover = process_color
|
||||
process_scrollbar_background_active = process_color
|
||||
|
||||
process_link_color = process_color
|
||||
process_link_background = process_color
|
||||
process_hover_color = process_color
|
||||
process_hover_background = process_color
|
||||
|
||||
def process_text_style(self, name: str, tokens: list[Token]) -> None:
|
||||
for token in tokens:
|
||||
value = token.value
|
||||
@@ -627,7 +632,10 @@ class StylesBuilder:
|
||||
)
|
||||
|
||||
style_definition = " ".join(token.value for token in tokens)
|
||||
self.styles.text_style = style_definition
|
||||
self.styles._rules[name.replace("-", "_")] = style_definition
|
||||
|
||||
process_link_style = process_text_style
|
||||
process_hover_style = process_text_style
|
||||
|
||||
def process_text_align(self, name: str, tokens: list[Token]) -> None:
|
||||
"""Process a text-align declaration"""
|
||||
|
||||
@@ -160,6 +160,14 @@ class RulesMap(TypedDict, total=False):
|
||||
|
||||
text_align: TextAlign
|
||||
|
||||
link_color: Color
|
||||
link_background: Color
|
||||
link_style: Style
|
||||
|
||||
hover_color: Color
|
||||
hover_background: Color
|
||||
hover_style: Style
|
||||
|
||||
|
||||
RULE_NAMES = list(RulesMap.__annotations__.keys())
|
||||
RULE_NAMES_SET = frozenset(RULE_NAMES)
|
||||
@@ -197,6 +205,10 @@ class StylesBase(ABC):
|
||||
"scrollbar_background",
|
||||
"scrollbar_background_hover",
|
||||
"scrollbar_background_active",
|
||||
"link_color",
|
||||
"link_background",
|
||||
"hover_color",
|
||||
"hover_background",
|
||||
}
|
||||
|
||||
node: DOMNode | None = None
|
||||
@@ -284,6 +296,14 @@ class StylesBase(ABC):
|
||||
|
||||
text_align = StringEnumProperty(VALID_TEXT_ALIGN, "start")
|
||||
|
||||
link_color = ColorProperty("transparent")
|
||||
link_background = ColorProperty("transparent")
|
||||
link_style = StyleFlagsProperty()
|
||||
|
||||
hover_color = ColorProperty("transparent")
|
||||
hover_background = ColorProperty("transparent")
|
||||
hover_style = StyleFlagsProperty()
|
||||
|
||||
def __eq__(self, styles: object) -> bool:
|
||||
"""Check that Styles contains the same rules."""
|
||||
if not isinstance(styles, StylesBase):
|
||||
|
||||
@@ -115,7 +115,6 @@ class ColorSystem:
|
||||
|
||||
dark = self._dark
|
||||
luminosity_spread = self._luminosity_spread
|
||||
text_alpha = self._text_alpha
|
||||
|
||||
if dark:
|
||||
background = self.background or Color.parse(DEFAULT_DARK_BACKGROUND)
|
||||
@@ -124,6 +123,8 @@ class ColorSystem:
|
||||
background = self.background or Color.parse(DEFAULT_LIGHT_BACKGROUND)
|
||||
surface = self.surface or Color.parse(DEFAULT_LIGHT_SURFACE)
|
||||
|
||||
foreground = background.inverse
|
||||
|
||||
boost = self.boost or background.get_contrast_text(1.0).with_alpha(0.07)
|
||||
|
||||
if self.panel is None:
|
||||
@@ -159,6 +160,7 @@ class ColorSystem:
|
||||
("primary-background", primary),
|
||||
("secondary-background", secondary),
|
||||
("background", background),
|
||||
("foregroud", foreground),
|
||||
("panel", panel),
|
||||
("boost", boost),
|
||||
("surface", surface),
|
||||
|
||||
@@ -280,6 +280,7 @@ class Screen(Widget):
|
||||
screen_y=event.screen_y,
|
||||
style=event.style,
|
||||
)
|
||||
widget.hover_style = event.style
|
||||
mouse_event._set_forwarded()
|
||||
await widget._forward_event(mouse_event)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import rich.repr
|
||||
from rich.color import Color
|
||||
from rich.console import ConsoleOptions, RenderableType, RenderResult
|
||||
from rich.segment import Segment, Segments
|
||||
from rich.style import Style, StyleType
|
||||
from rich.style import NULL_STYLE, Style, StyleType
|
||||
|
||||
from . import events
|
||||
from ._types import MessageTarget
|
||||
@@ -118,8 +118,8 @@ class ScrollBarRender:
|
||||
start_index, start_bar = divmod(max(0, start), len_bars)
|
||||
end_index, end_bar = divmod(max(0, end), len_bars)
|
||||
|
||||
upper = {"@click": "scroll_up"}
|
||||
lower = {"@click": "scroll_down"}
|
||||
upper = {"@mouse.up": "scroll_up"}
|
||||
lower = {"@mouse.up": "scroll_down"}
|
||||
|
||||
upper_back_segment = Segment(blank, _Style(bgcolor=back, meta=upper))
|
||||
lower_back_segment = Segment(blank, _Style(bgcolor=back, meta=lower))
|
||||
@@ -189,6 +189,17 @@ class ScrollBarRender:
|
||||
|
||||
@rich.repr.auto
|
||||
class ScrollBar(Widget):
|
||||
|
||||
DEFAULT_CSS = """
|
||||
ScrollBar {
|
||||
hover-color: ;
|
||||
hover-background:;
|
||||
hover-style: ;
|
||||
link-color: transparent;
|
||||
link-background: transparent;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, vertical: bool = True, name: str | None = None, *, thickness: int = 1
|
||||
) -> None:
|
||||
@@ -211,6 +222,17 @@ class ScrollBar(Widget):
|
||||
if self.thickness > 1:
|
||||
yield "thickness", self.thickness
|
||||
|
||||
@property
|
||||
def link_style(self) -> Style:
|
||||
return NULL_STYLE
|
||||
|
||||
@property
|
||||
def link_hover_style(self) -> Style:
|
||||
return NULL_STYLE
|
||||
|
||||
def watch_hover_style(self, old_style: Style, new_style: Style) -> None:
|
||||
pass
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
styles = self.parent.styles
|
||||
background = (
|
||||
|
||||
@@ -10,13 +10,14 @@ import rich.repr
|
||||
from rich.console import (
|
||||
Console,
|
||||
ConsoleRenderable,
|
||||
ConsoleOptions,
|
||||
RichCast,
|
||||
JustifyMethod,
|
||||
RenderableType,
|
||||
RenderResult,
|
||||
)
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
from rich.styled import Styled
|
||||
from rich.style import Style, StyleType
|
||||
from rich.text import Text
|
||||
|
||||
from . import errors, events, messages
|
||||
@@ -57,6 +58,49 @@ _JUSTIFY_MAP: dict[str, JustifyMethod] = {
|
||||
}
|
||||
|
||||
|
||||
class _Styled:
|
||||
"""Apply a style to a renderable.
|
||||
|
||||
Args:
|
||||
renderable (RenderableType): Any renderable.
|
||||
style (StyleType): A style to apply across the entire renderable.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, renderable: "RenderableType", style: Style, link_style: Style
|
||||
) -> None:
|
||||
self.renderable = renderable
|
||||
self.style = style
|
||||
self.link_style = link_style
|
||||
|
||||
def __rich_console__(
|
||||
self, console: "Console", options: "ConsoleOptions"
|
||||
) -> "RenderResult":
|
||||
style = console.get_style(self.style)
|
||||
result_segments = console.render(self.renderable, options)
|
||||
|
||||
_Segment = Segment
|
||||
if style:
|
||||
apply = style.__add__
|
||||
result_segments = (
|
||||
_Segment(text, apply(_style), control)
|
||||
for text, _style, control in result_segments
|
||||
)
|
||||
link_style = self.link_style
|
||||
if link_style:
|
||||
result_segments = (
|
||||
_Segment(
|
||||
text,
|
||||
style
|
||||
if style._meta is None
|
||||
else (link_style + style if "click" in style.meta else style),
|
||||
control,
|
||||
)
|
||||
for text, style, control in result_segments
|
||||
)
|
||||
return result_segments
|
||||
|
||||
|
||||
class RenderCache(NamedTuple):
|
||||
"""Stores results of a previous render."""
|
||||
|
||||
@@ -82,6 +126,9 @@ class Widget(DOMNode):
|
||||
scrollbar-corner-color: $panel-darken-1;
|
||||
scrollbar-size-vertical: 2;
|
||||
scrollbar-size-horizontal: 1;
|
||||
link-style: underline;
|
||||
hover-background: $boost;
|
||||
hover-style: not underline;
|
||||
}
|
||||
"""
|
||||
COMPONENT_CLASSES: ClassVar[set[str]] = set()
|
||||
@@ -95,6 +142,8 @@ class Widget(DOMNode):
|
||||
shrink = Reactive(True)
|
||||
"""Rich renderable may shrink."""
|
||||
|
||||
hover_style: Reactive[Style] = Reactive(Style)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*children: Widget,
|
||||
@@ -131,7 +180,7 @@ class Widget(DOMNode):
|
||||
|
||||
self._styles_cache = StylesCache()
|
||||
self._rich_style_cache: dict[str, Style] = {}
|
||||
|
||||
self._link_styles: dict[str, Style] = {}
|
||||
self._lock = Lock()
|
||||
|
||||
super().__init__(
|
||||
@@ -383,6 +432,13 @@ class Widget(DOMNode):
|
||||
|
||||
return height
|
||||
|
||||
def watch_hover_style(self, old_style: Style, new_style: Style) -> None:
|
||||
self._link_styles.pop(old_style.link_id, None)
|
||||
if new_style.link_id:
|
||||
meta = new_style.meta
|
||||
if "@click" in meta:
|
||||
self._link_styles[new_style.link_id] = self.link_hover_style
|
||||
|
||||
def watch_scroll_x(self, new_value: float) -> None:
|
||||
self.horizontal_scrollbar.position = int(new_value)
|
||||
self.refresh(layout=True)
|
||||
@@ -798,6 +854,26 @@ class Widget(DOMNode):
|
||||
return node.styles.layers
|
||||
return ("default",)
|
||||
|
||||
@property
|
||||
def link_style(self) -> Style:
|
||||
styles = self.styles
|
||||
base, _, background, color = self.colors
|
||||
style = styles.link_style + Style.from_color(
|
||||
(color + styles.link_color).rich_color,
|
||||
(background + styles.link_background).rich_color,
|
||||
)
|
||||
return style
|
||||
|
||||
@property
|
||||
def link_hover_style(self) -> Style:
|
||||
styles = self.styles
|
||||
base, _, background, color = self.colors
|
||||
style = styles.hover_style + Style.from_color(
|
||||
(color + styles.hover_color).rich_color,
|
||||
(background + styles.hover_background).rich_color,
|
||||
)
|
||||
return style
|
||||
|
||||
def _set_dirty(self, *regions: Region) -> None:
|
||||
"""Set the Widget as 'dirty' (requiring re-paint).
|
||||
|
||||
@@ -1413,7 +1489,7 @@ class Widget(DOMNode):
|
||||
):
|
||||
renderable.justify = text_justify
|
||||
|
||||
renderable = Styled(renderable, self.rich_style)
|
||||
renderable = _Styled(renderable, self.rich_style, self.link_style)
|
||||
|
||||
return renderable
|
||||
|
||||
|
||||
@@ -107,8 +107,11 @@ class Coord(NamedTuple):
|
||||
class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
|
||||
DEFAULT_CSS = """
|
||||
App.-dark DataTable {
|
||||
background:;
|
||||
}
|
||||
DataTable {
|
||||
|
||||
background: $surface ;
|
||||
color: $text;
|
||||
}
|
||||
DataTable > .datatable--header {
|
||||
|
||||
@@ -79,20 +79,20 @@ class Header(Widget):
|
||||
color: $text;
|
||||
height: 1;
|
||||
}
|
||||
Header.tall {
|
||||
Header.-tall {
|
||||
height: 3;
|
||||
}
|
||||
"""
|
||||
|
||||
tall = Reactive(True)
|
||||
|
||||
DEFAULT_CLASSES = "tall"
|
||||
DEFAULT_CLASSES = "-tall"
|
||||
|
||||
def watch_tall(self, tall: bool) -> None:
|
||||
self.set_class(tall, "tall")
|
||||
self.set_class(tall, "-tall")
|
||||
|
||||
async def on_click(self, event):
|
||||
self.toggle_class("tall")
|
||||
self.toggle_class("-tall")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
def set_title(title: str) -> None:
|
||||
|
||||
@@ -86,11 +86,11 @@ class Static(Widget):
|
||||
"""
|
||||
return self._renderable
|
||||
|
||||
def update(self, renderable: RenderableType) -> None:
|
||||
def update(self, renderable: RenderableType = "") -> None:
|
||||
"""Update the widget's content area with new text or Rich renderable.
|
||||
|
||||
Args:
|
||||
renderable (RenderableType, optional): A new rich renderable.
|
||||
renderable (RenderableType, optional): A new rich renderable. Defaults to empty renderable;
|
||||
"""
|
||||
_check_renderable(renderable)
|
||||
self.renderable = renderable
|
||||
|
||||
@@ -164,10 +164,10 @@ class TreeNode(Generic[NodeDataType]):
|
||||
class TreeControl(Generic[NodeDataType], Static, can_focus=True):
|
||||
DEFAULT_CSS = """
|
||||
TreeControl {
|
||||
|
||||
color: $text;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
link-style: not underline;
|
||||
}
|
||||
|
||||
TreeControl > .tree--guides {
|
||||
@@ -324,8 +324,8 @@ class TreeControl(Generic[NodeDataType], Static, can_focus=True):
|
||||
if isinstance(node.label, str)
|
||||
else node.label
|
||||
)
|
||||
if node.id == self.hover_node:
|
||||
label.stylize("underline")
|
||||
# if node.id == self.hover_node:
|
||||
# label.stylize("underline")
|
||||
label.apply_meta({"@click": f"click_label({node.id})", "tree_node": node.id})
|
||||
return label
|
||||
|
||||
|
||||
Reference in New Issue
Block a user