code browser and fixes

This commit is contained in:
Will McGugan
2022-09-08 11:00:05 +01:00
parent f1f7a620af
commit afd22265f4
10 changed files with 120 additions and 31 deletions

26
examples/code_browser.css Normal file
View File

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

49
examples/code_browser.py Normal file
View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ class Container(Widget):
Container {
layout: vertical;
overflow: auto;
}
"""

View File

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

View File

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

View File

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

View File

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

View File

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