mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
code browser and fixes
This commit is contained in:
26
examples/code_browser.css
Normal file
26
examples/code_browser.css
Normal 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
49
examples/code_browser.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -8,6 +8,7 @@ class Container(Widget):
|
||||
Container {
|
||||
layout: vertical;
|
||||
overflow: auto;
|
||||
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user