This commit is contained in:
Will McGugan
2021-10-25 13:50:00 +01:00
parent e2faad8f46
commit 942b00f255
11 changed files with 189 additions and 113 deletions

31
examples/basic.py Normal file
View File

@@ -0,0 +1,31 @@
from textual.app import App
from textual.widgets import Placeholder
class BasicApp(App):
"""Demonstrates smooth animation. Press 'b' to see it in action."""
css = """
App > View {
layout: dock
}
#widget1 {
edge: top
}
#widget2 {
}
"""
async def on_mount(self) -> None:
"""Build layout here."""
await self.view.mount(widget1=Placeholder(), widget2=Placeholder())
SmoothApp.run(log="textual.log")

View File

@@ -1,6 +1,5 @@
from rich.markdown import Markdown from rich.markdown import Markdown
from textual import events
from textual.app import App from textual.app import App
from textual.widgets import Header, Footer, Placeholder, ScrollView from textual.widgets import Header, Footer, Placeholder, ScrollView
@@ -8,24 +7,38 @@ from textual.widgets import Header, Footer, Placeholder, ScrollView
class MyApp(App): class MyApp(App):
"""An example of a very simple Textual App""" """An example of a very simple Textual App"""
async def on_load(self, event: events.Load) -> None: stylesheet = """
App > View {
layout: dock
}
#body {
padding: 1
}
#sidebar {
edge left
size: 40
}
"""
async def on_load(self) -> None:
"""Bind keys with the app loads (but before entering application mode)""" """Bind keys with the app loads (but before entering application mode)"""
await self.bind("b", "view.toggle('sidebar')", "Toggle sidebar") await self.bind("b", "view.toggle('sidebar')", "Toggle sidebar")
await self.bind("q", "quit", "Quit") await self.bind("q", "quit", "Quit")
async def on_mount(self, event: events.Mount) -> None: async def on_mount(self) -> None:
"""Create and dock the widgets.""" """Create and dock the widgets."""
# A scrollview to contain the markdown file body = ScrollView()
body = ScrollView(gutter=1) await self.view.mount(
Header(),
# Header / footer / dock Footer(),
await self.view.dock(Header(), edge="top") body=body,
await self.view.dock(Footer(), edge="bottom") sidebar=Placeholder(),
await self.view.dock(Placeholder(), edge="left", size=30, name="sidebar") )
# Dock the body in the remaining space
await self.view.dock(body, edge="right")
async def get_markdown(filename: str) -> None: async def get_markdown(filename: str) -> None:
with open(filename, "rt") as fh: with open(filename, "rt") as fh:
@@ -35,4 +48,4 @@ class MyApp(App):
await self.call_later(get_markdown, "richreadme.md") await self.call_later(get_markdown, "richreadme.md")
MyApp.run(title="Simple App", log="textual.log", css_file="theme.css") MyApp.run(title="Simple App", log="textual.log")

View File

@@ -71,6 +71,7 @@ class App(MessagePump):
log_verbosity: int = 1, log_verbosity: int = 1,
title: str = "Textual Application", title: str = "Textual Application",
css_file: str | None = None, css_file: str | None = None,
css: str | None = None,
): ):
"""The Textual Application base class """The Textual Application base class
@@ -112,6 +113,7 @@ class App(MessagePump):
self.stylesheet = Stylesheet() self.stylesheet = Stylesheet()
self.css_file = css_file self.css_file = css_file
self.css = css
super().__init__() super().__init__()
@@ -299,7 +301,8 @@ class App(MessagePump):
try: try:
if self.css_file is not None: if self.css_file is not None:
self.stylesheet.read(self.css_file) self.stylesheet.read(self.css_file)
print(self.stylesheet.css) if self.css is not None:
self.stylesheet.parse(self.css)
except Exception: except Exception:
self.panic() self.panic()

View File

@@ -15,7 +15,7 @@ if TYPE_CHECKING:
from .styles import Styles from .styles import Styles
class _BoxProperty: class BoxProperty:
DEFAULT = ("", Style()) DEFAULT = ("", Style())
@@ -67,7 +67,7 @@ class Edges(NamedTuple):
yield "left", left yield "left", left
class _BorderProperty: class BorderProperty:
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self._properties = ( self._properties = (
f"{name}_top", f"{name}_top",
@@ -129,14 +129,21 @@ class _BorderProperty:
raise StyleValueError("expected 1, 2, or 4 values") raise StyleValueError("expected 1, 2, or 4 values")
class _StyleProperty: class StyleProperty:
DEFAULT_STYLE = Style()
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self._internal_name = f"_{name}" self._internal_name = f"_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style: def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style:
return getattr(obj, self._internal_name) return getattr(obj, self._internal_name) or self.DEFAULT_STYLE
def __set__(self, obj: Styles, style: Style | str | None) -> Style | str | None:
if style is None:
setattr(obj, self._internal_name, None)
return None
def __set__(self, obj: Styles, style: Style | str) -> Style:
if isinstance(style, str): if isinstance(style, str):
_style = Style.parse(style) _style = Style.parse(style)
else: else:
@@ -145,7 +152,7 @@ class _StyleProperty:
return _style return _style
class _SpacingProperty: class SpacingProperty:
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self._internal_name = f"_{name}" self._internal_name = f"_{name}"
@@ -158,7 +165,7 @@ class _SpacingProperty:
return spacing return spacing
class _DocksProperty: class DocksProperty:
def __get__( def __get__(
self, obj: Styles, objtype: type[Styles] | None = None self, obj: Styles, objtype: type[Styles] | None = None
) -> tuple[str, ...]: ) -> tuple[str, ...]:
@@ -173,7 +180,7 @@ class _DocksProperty:
return _docks return _docks
class _DockGroupProperty: class DockGroupProperty:
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str: def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str:
return obj._dock_group or "" return obj._dock_group or ""
@@ -182,7 +189,7 @@ class _DockGroupProperty:
return spacing return spacing
class _OffsetProperty: class OffsetProperty:
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self._internal_name = f"_{name}" self._internal_name = f"_{name}"
@@ -195,7 +202,7 @@ class _OffsetProperty:
return offset return offset
class _DockEdgeProperty: class DockEdgeProperty:
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str: def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str:
return obj._dock_edge or "" return obj._dock_edge or ""
@@ -204,3 +211,37 @@ class _DockEdgeProperty:
raise ValueError(f"dock edge must be one of {friendly_list(VALID_EDGE)}") raise ValueError(f"dock edge must be one of {friendly_list(VALID_EDGE)}")
obj._dock_edge = edge obj._dock_edge = edge
return edge return edge
class IntegerProperty:
def __set_name__(self, owner: Styles, name: str) -> None:
self._internal_name = f"_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> int:
return getattr(obj, self._internal_name, 0)
def __set__(self, obj: Styles, value: int | None) -> int | None:
setattr(obj, self._internal_name, value)
return value
class StringProperty:
def __init__(self, valid_values: set[str], default: str) -> None:
self._valid_values = valid_values
self._default = default
def __set_name__(self, owner: Styles, name: str) -> None:
self._name = name
self._internal_name = f"_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str:
return getattr(obj, self._internal_name, self._default)
def __set__(self, obj: Styles, value: str | None = None) -> str | None:
if value is not None:
if value not in self._valid_values:
raise StyleValueError(
f"{self._name} must be one of {friendly_list(self._valid_values)}"
)
setattr(obj, self._internal_name, value)
return value

View File

@@ -16,14 +16,16 @@ from .constants import (
) )
from ..geometry import NULL_OFFSET, Offset, Spacing from ..geometry import NULL_OFFSET, Offset, Spacing
from ._style_properties import ( from ._style_properties import (
_BorderProperty, BorderProperty,
_BoxProperty, BoxProperty,
_DockEdgeProperty, DockEdgeProperty,
_DocksProperty, DocksProperty,
_DockGroupProperty, DockGroupProperty,
_OffsetProperty, IntegerProperty,
_SpacingProperty, OffsetProperty,
_StyleProperty, SpacingProperty,
StringProperty,
StyleProperty,
) )
from .types import Display, Visibility from .types import Display, Visibility
@@ -33,10 +35,10 @@ class Styles:
_display: Display | None = None _display: Display | None = None
_visibility: Visibility | None = None _visibility: Visibility | None = None
_layout: str | None = None
_text: Style = Style() _text: Style | None = None
_layout: str = ""
_padding: Spacing | None = None _padding: Spacing | None = None
_margin: Spacing | None = None _margin: Spacing | None = None
_offset: Offset | None = None _offset: Offset | None = None
@@ -51,71 +53,45 @@ class Styles:
_outline_bottom: tuple[str, Style] | None = None _outline_bottom: tuple[str, Style] | None = None
_outline_left: tuple[str, Style] | None = None _outline_left: tuple[str, Style] | None = None
_size: int | None = None
_fraction: int | None = None
_min_size: int | None = None
_dock_group: str | None = None _dock_group: str | None = None
_dock_edge: str | None = None _dock_edge: str | None = None
_docks: tuple[str, ...] | None = None _docks: tuple[str, ...] | None = None
important: set[str] = field(default_factory=set) important: set[str] = field(default_factory=set)
@property display = StringProperty(VALID_DISPLAY, "block")
def display(self) -> Display: visibility = StringProperty(VALID_VISIBILITY, "visible")
return self._display or "block" layout = StringProperty(VALID_LAYOUT, "dock")
@display.setter text = StyleProperty()
def display(self, display: Display) -> None:
if display not in VALID_DISPLAY:
raise StyleValueError(
f"display must be one of {friendly_list(VALID_DISPLAY)}"
)
self._display = display
@property padding = SpacingProperty()
def visibility(self) -> Visibility: margin = SpacingProperty()
return self._visibility or "visible" offset = OffsetProperty()
@visibility.setter border = BorderProperty()
def visibility(self, visibility: Visibility) -> None: border_top = BoxProperty()
if visibility not in VALID_VISIBILITY: border_right = BoxProperty()
raise StyleValueError( border_bottom = BoxProperty()
f"visibility must be one of {friendly_list(VALID_VISIBILITY)}" border_left = BoxProperty()
)
self._visibility = visibility
text = _StyleProperty() outline = BorderProperty()
outline_top = BoxProperty()
outline_right = BoxProperty()
outline_bottom = BoxProperty()
outline_left = BoxProperty()
@property size = IntegerProperty()
def layout(self) -> str: fraction = IntegerProperty()
return self._layout min_size = IntegerProperty()
@layout.setter dock_group = DockGroupProperty()
def layout(self, layout: str) -> None: docks = DocksProperty()
if layout not in VALID_LAYOUT: dock_edge = DockEdgeProperty()
raise StyleValueError(
f"layout must be one of {friendly_list(VALID_LAYOUT)}"
)
self._layout = layout
offset = _OffsetProperty()
padding = _SpacingProperty()
margin = _SpacingProperty()
border = _BorderProperty()
border_top = _BoxProperty()
border_right = _BoxProperty()
border_bottom = _BoxProperty()
border_left = _BoxProperty()
outline = _BorderProperty()
outline_top = _BoxProperty()
outline_right = _BoxProperty()
outline_bottom = _BoxProperty()
outline_left = _BoxProperty()
dock_group = _DockGroupProperty()
docks = _DocksProperty()
dock_edge = _DockEdgeProperty()
@property @property
def has_border(self) -> bool: def has_border(self) -> bool:
@@ -238,7 +214,6 @@ if __name__ == "__main__":
styles.docks = "foo bar" styles.docks = "foo bar"
styles.text = "italic blue" styles.text = "italic blue"
styles.dock_group = "bar" styles.dock_group = "bar"
styles.dock_edge = "sdfsdf"
from rich import inspect, print from rich import inspect, print

View File

@@ -41,10 +41,6 @@ class Dock:
class DockLayout(Layout): class DockLayout(Layout):
def __init__(self, docks: list[Dock] = None) -> None:
self.docks: list[Dock] = docks or []
super().__init__()
def get_widgets(self) -> Iterable[Widget]: def get_widgets(self) -> Iterable[Widget]:
for dock in self.docks: for dock in self.docks:
yield from dock.widgets yield from dock.widgets

View File

@@ -41,7 +41,9 @@ class View(Widget):
layout_factory: ClassVar[Callable[[], Layout]] layout_factory: ClassVar[Callable[[], Layout]]
def __init__(self, layout: Layout = None, name: str | None = None) -> None: def __init__(
self, layout: Layout = None, name: str | None = None, id: str | None = None
) -> None:
self._layout: Layout = layout or self.layout_factory() self._layout: Layout = layout or self.layout_factory()
self.mouse_over: Widget | None = None self.mouse_over: Widget | None = None
@@ -55,7 +57,7 @@ class View(Widget):
[], [],
) )
super().__init__(name=name) super().__init__(name=name, id=id)
def __init_subclass__( def __init_subclass__(
cls, layout: Callable[[], Layout] | None = None, **kwargs cls, layout: Callable[[], Layout] | None = None, **kwargs
@@ -144,7 +146,7 @@ class View(Widget):
for name, widget in name_widgets: for name, widget in name_widgets:
if name is not None: if name is not None:
widget.name = name widget.name = name
self.add_child(widget) self._add_child(widget)
self.refresh() self.refresh()

View File

@@ -21,16 +21,21 @@ class DockView(View):
async def dock( async def dock(
self, self,
*widgets: Widget, *widgets: Widget,
name: str | None = None,
id: str | None = None,
edge: DockEdge = "top", edge: DockEdge = "top",
z: int = 0, z: int = 0,
size: int | None | DoNotSet = do_not_set, size: int | None | DoNotSet = do_not_set,
name: str | None = None,
) -> None: ) -> None:
dock = Dock(edge, widgets, z) dock = Dock(edge, widgets, z)
assert isinstance(self._layout, DockLayout) assert isinstance(self._layout, DockLayout)
self._layout.docks.append(dock) self._layout.docks.append(dock)
for widget in widgets: for widget in widgets:
if id is not None:
widget._id = id
if name is not None:
widget.name = name
if size is not do_not_set: if size is not do_not_set:
widget.layout_size = cast(Optional[int], size) widget.layout_size = cast(Optional[int], size)
if name is None: if name is None:
@@ -42,17 +47,18 @@ class DockView(View):
async def dock_grid( async def dock_grid(
self, self,
*, *,
name: str | None = None,
id: str | None = None,
edge: DockEdge = "top", edge: DockEdge = "top",
z: int = 0, z: int = 0,
size: int | None | DoNotSet = do_not_set, size: int | None | DoNotSet = do_not_set,
name: str | None = None,
gap: tuple[int, int] | int | None = None, gap: tuple[int, int] | int | None = None,
gutter: tuple[int, int] | int | None = None, gutter: tuple[int, int] | int | None = None,
align: tuple[GridAlign, GridAlign] | None = None, align: tuple[GridAlign, GridAlign] | None = None,
) -> GridLayout: ) -> GridLayout:
grid = GridLayout(gap=gap, gutter=gutter, align=align) grid = GridLayout(gap=gap, gutter=gutter, align=align)
view = View(layout=grid, name=name) view = View(layout=grid, id=id, name=name)
dock = Dock(edge, (view,), z) dock = Dock(edge, (view,), z)
assert isinstance(self._layout, DockLayout) assert isinstance(self._layout, DockLayout)
self._layout.docks.append(dock) self._layout.docks.append(dock)

View File

@@ -85,7 +85,6 @@ class Widget(MessagePump):
super().__init__() super().__init__()
id: str | None = None
visible: Reactive[bool] = Reactive(True, layout=True) visible: Reactive[bool] = Reactive(True, layout=True)
layout_size: Reactive[int | None] = Reactive(None, layout=True) layout_size: Reactive[int | None] = Reactive(None, layout=True)
layout_fraction: Reactive[int] = Reactive(1, layout=True) layout_fraction: Reactive[int] = Reactive(1, layout=True)
@@ -124,7 +123,7 @@ class Widget(MessagePump):
renderable = self.render_styled() renderable = self.render_styled()
return renderable return renderable
def add_child(self, widget: Widget) -> Widget: def _add_child(self, widget: Widget) -> Widget:
"""Add a child widget. """Add a child widget.
Args: Args:
@@ -134,12 +133,21 @@ class Widget(MessagePump):
self.children._append(widget) self.children._append(widget)
return widget return widget
def get_child(self, name: str | None = None) -> Widget: def get_child(self, name: str | None = None, id: str | None = None) -> Widget:
for widget in self.children: if name is not None:
if widget.name == name: for widget in self.children:
return widget if widget.name == name:
return widget
if id is not None:
for widget in self.children:
if widget.id == id:
return widget
raise errors.MissingWidget(f"Widget named {name!r} was not found in {self}") raise errors.MissingWidget(f"Widget named {name!r} was not found in {self}")
@property
def id(self) -> str | None:
return self._id
@property @property
def class_names(self) -> frozenset[str]: def class_names(self) -> frozenset[str]:
return frozenset(self._class_names) return frozenset(self._class_names)
@@ -188,6 +196,7 @@ class Widget(MessagePump):
if self.padding is not None: if self.padding is not None:
renderable = Padding(renderable, self.padding) renderable = Padding(renderable, self.padding)
if self.border not in ("", "none"): if self.border not in ("", "none"):
1 / 0
_border_style = self.console.get_style(self.border_style) _border_style = self.console.get_style(self.border_style)
renderable = Border( renderable = Border(
renderable, renderable,

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
from datetime import datetime from datetime import datetime
from logging import getLogger from logging import getLogger

View File

@@ -35,14 +35,12 @@ class ScrollView(View):
super().__init__(name=name, layout=layout) super().__init__(name=name, layout=layout)
self.fluid = fluid self.fluid = fluid
self.vscroll = self.add_child(ScrollBar(vertical=True)) self.vscroll = ScrollBar(vertical=True)
self.hscroll = self.add_child(ScrollBar(vertical=False)) self.hscroll = ScrollBar(vertical=False)
self.window = self.add_child( self.window = WindowView(
WindowView( "" if contents is None else contents,
"" if contents is None else contents, auto_width=auto_width,
auto_width=auto_width, gutter=gutter,
gutter=gutter,
)
) )
layout.add_column("main") layout.add_column("main")