diff --git a/examples/code_browser.css b/examples/code_browser.css new file mode 100644 index 000000000..f2b476035 --- /dev/null +++ b/examples/code_browser.css @@ -0,0 +1,26 @@ +#tree-view { + display: none; + scrollbar-gutter: stable; +} + +CodeBrowser.-show-tree #tree-view { + display: block; + dock: left; + height: 100%; + width: auto; + background: $surface; + +} + +CodeBrowser{ + background: $surface-darken-1; +} + +DirectoryTree { + padding-right: 1; + +} + +#code { + width: auto; +} diff --git a/examples/code_browser.py b/examples/code_browser.py new file mode 100644 index 000000000..93e06056d --- /dev/null +++ b/examples/code_browser.py @@ -0,0 +1,49 @@ +import sys + +from rich.syntax import Syntax +from rich.traceback import Traceback +from textual.app import App, ComposeResult +from textual.layout import Container, Vertical +from textual.reactive import Reactive +from textual.widgets import DirectoryTree, Footer, Header, Static + + +class CodeBrowser(App): + + show_tree = Reactive.init(True) + + def watch_show_tree(self, show_tree: bool) -> None: + self.set_class(show_tree, "-show-tree") + + def on_load(self) -> None: + self.bind("t", "toggle_tree", description="Toggle Tree") + self.bind("q", "quit", description="Quit") + + def compose(self) -> ComposeResult: + path = "./" if len(sys.argv) < 2 else sys.argv[1] + yield Header() + yield Container( + Vertical(DirectoryTree(path), id="tree-view"), + Vertical(Static(id="code"), id="code-view"), + ) + yield Footer() + + def on_directory_tree_file_click(self, event: DirectoryTree.FileClick) -> None: + code_view = self.query_one("#code", Static) + try: + syntax = Syntax.from_path(event.path, line_numbers=True, word_wrap=True) + except Exception: + code_view.update(Traceback()) + self.sub_title = "ERROR" + else: + code_view.update(syntax) + self.query_one("#code-view").scroll_home(animate=False) + self.sub_title = event.path + + def action_toggle_tree(self) -> None: + self.show_tree = not self.show_tree + + +app = CodeBrowser(css_path="code_browser.css") +if __name__ == "__main__": + app.run() diff --git a/src/textual/_layout.py b/src/textual/_layout.py index a59ded61e..49284bddb 100644 --- a/src/textual/_layout.py +++ b/src/textual/_layout.py @@ -65,9 +65,14 @@ class Layout(ABC): int: Width of the content. """ width: int | None = None + widget_gutter = widget.gutter.width for child in widget.displayed_children: if not child.is_container: - child_width = child.get_content_width(container, viewport) + child_width = ( + child.get_content_width(container, viewport) + + widget_gutter + + child.gutter.width + ) width = child_width if width is None else max(width, child_width) if width is None: width = container.width diff --git a/src/textual/dom.py b/src/textual/dom.py index 70bc64924..e21b440a9 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -544,7 +544,7 @@ class DOMNode(MessagePump): return nodes @property - def displayed_children(self) -> list[DOMNode]: + def displayed_children(self) -> list[Widget]: """The children which don't have display: none set. Returns: diff --git a/src/textual/layout.py b/src/textual/layout.py index b06c347c3..ae22aaaf5 100644 --- a/src/textual/layout.py +++ b/src/textual/layout.py @@ -8,6 +8,7 @@ class Container(Widget): Container { layout: vertical; overflow: auto; + } """ diff --git a/src/textual/widget.py b/src/textual/widget.py index 65b847d23..ea0140b7f 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -7,8 +7,13 @@ from operator import attrgetter from typing import TYPE_CHECKING, ClassVar, Collection, Iterable, NamedTuple import rich.repr -from rich.console import Console, JustifyMethod, RenderableType -from rich.measure import Measurement +from rich.console import ( + Console, + ConsoleRenderable, + Measurement, + JustifyMethod, + RenderableType, +) from rich.segment import Segment from rich.style import Style from rich.styled import Styled @@ -313,17 +318,15 @@ class Widget(DOMNode): """ if self.is_container: assert self._layout is not None - return ( - self._layout.get_content_width(self, container, viewport) - + self.scrollbar_size_vertical - ) + return self._layout.get_content_width(self, container, viewport) cache_key = container.width if self._content_width_cache[0] == cache_key: return self._content_width_cache[1] console = self.app.console - renderable = self.render() + renderable = self.post_render(self.render()) + measurement = Measurement.get( console, console.options.update_width(container.width), @@ -509,18 +512,18 @@ class Widget(DOMNode): @property def scrollbar_size_vertical(self) -> int: """Get the width used by the *vertical* scrollbar.""" - return ( - self.styles.scrollbar_size_vertical if self.show_vertical_scrollbar else 0 - ) + styles = self.styles + if styles.scrollbar_gutter == "stable" and styles.overflow_y == "auto": + return styles.scrollbar_size_vertical + return styles.scrollbar_size_vertical if self.show_vertical_scrollbar else 0 @property def scrollbar_size_horizontal(self) -> int: """Get the height used by the *horizontal* scrollbar.""" - return ( - self.styles.scrollbar_size_horizontal - if self.show_horizontal_scrollbar - else 0 - ) + styles = self.styles + if styles.scrollbar_gutter == "stable" and styles.overflow_x == "auto": + return self.styles.scrollbar_size_horizontal + return styles.scrollbar_size_horizontal if self.show_horizontal_scrollbar else 0 @property def scrollbar_gutter(self) -> Spacing: @@ -1214,7 +1217,7 @@ class Widget(DOMNode): if self.descendant_has_focus: yield "focus-within" - def post_render(self, renderable: RenderableType) -> RenderableType: + def post_render(self, renderable: RenderableType) -> ConsoleRenderable: """Applies style attributes to the default renderable. Returns: diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index d718888de..9512641f1 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -21,14 +21,13 @@ class DirEntry: is_dir: bool -@rich.repr.auto -class FileClick(Message, bubble=True): - def __init__(self, sender: MessageTarget, path: str) -> None: - self.path = path - super().__init__(sender) - - class DirectoryTree(TreeControl[DirEntry]): + @rich.repr.auto + class FileClick(Message, bubble=True): + def __init__(self, sender: MessageTarget, path: str) -> None: + self.path = path + super().__init__(sender) + def __init__( self, path: str, @@ -112,7 +111,7 @@ class DirectoryTree(TreeControl[DirEntry]): ) -> None: dir_entry = message.node.data if not dir_entry.is_dir: - await self.emit(FileClick(self, dir_entry.path)) + await self.emit(self.FileClick(self, dir_entry.path)) else: if not message.node.loaded: await self.load_directory(message.node) diff --git a/src/textual/widgets/_header.py b/src/textual/widgets/_header.py index 26cb4bea0..515c5e2c3 100644 --- a/src/textual/widgets/_header.py +++ b/src/textual/widgets/_header.py @@ -63,7 +63,8 @@ class HeaderTitle(Widget): def render(self) -> Text: text = Text(self.text, no_wrap=True, overflow="ellipsis") if self.sub_text: - text.append(f" - {self.sub_text}", "dim") + text.append(" — ") + text.append(self.sub_text, "dim") return text @@ -83,8 +84,13 @@ class Header(Widget): } """ + tall = Reactive(True) + DEFAULT_CLASSES = "tall" + def watch_tall(self, tall: bool) -> None: + self.set_class(tall, "tall") + async def on_click(self, event): self.toggle_class("tall") diff --git a/src/textual/widgets/_static.py b/src/textual/widgets/_static.py index 824248ce7..5e5c74a64 100644 --- a/src/textual/widgets/_static.py +++ b/src/textual/widgets/_static.py @@ -1,9 +1,9 @@ from __future__ import annotations from rich.console import RenderableType - from rich.protocol import is_renderable +from ..reactive import Reactive from ..errors import RenderError from ..widget import Widget @@ -49,4 +49,4 @@ class Static(Widget): def update(self, renderable: RenderableType) -> None: _check_renderable(renderable) self.renderable = renderable - self.refresh() + self.refresh(layout=True) diff --git a/src/textual/widgets/_tree_control.py b/src/textual/widgets/_tree_control.py index 05f8f5b13..51c05a92c 100644 --- a/src/textual/widgets/_tree_control.py +++ b/src/textual/widgets/_tree_control.py @@ -13,7 +13,7 @@ from ..geometry import Region, Size from .. import events from ..reactive import Reactive from .._types import MessageTarget -from ..widget import Widget +from ..widgets import Static from ..message import Message from .. import messages @@ -161,7 +161,7 @@ class TreeNode(Generic[NodeDataType]): return self._control.render_node(self) -class TreeControl(Generic[NodeDataType], Widget, can_focus=True): +class TreeControl(Generic[NodeDataType], Static, can_focus=True): DEFAULT_CSS = """ TreeControl { background: $surface;