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 textual import events
from textual.app import App
from textual.widgets import Header, Footer, Placeholder, ScrollView
@@ -8,24 +7,38 @@ from textual.widgets import Header, Footer, Placeholder, ScrollView
class MyApp(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)"""
await self.bind("b", "view.toggle('sidebar')", "Toggle sidebar")
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."""
# A scrollview to contain the markdown file
body = ScrollView(gutter=1)
# Header / footer / dock
await self.view.dock(Header(), edge="top")
await self.view.dock(Footer(), edge="bottom")
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")
body = ScrollView()
await self.view.mount(
Header(),
Footer(),
body=body,
sidebar=Placeholder(),
)
async def get_markdown(filename: str) -> None:
with open(filename, "rt") as fh:
@@ -35,4 +48,4 @@ class MyApp(App):
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,
title: str = "Textual Application",
css_file: str | None = None,
css: str | None = None,
):
"""The Textual Application base class
@@ -112,6 +113,7 @@ class App(MessagePump):
self.stylesheet = Stylesheet()
self.css_file = css_file
self.css = css
super().__init__()
@@ -299,7 +301,8 @@ class App(MessagePump):
try:
if self.css_file is not None:
self.stylesheet.read(self.css_file)
print(self.stylesheet.css)
if self.css is not None:
self.stylesheet.parse(self.css)
except Exception:
self.panic()

View File

@@ -15,7 +15,7 @@ if TYPE_CHECKING:
from .styles import Styles
class _BoxProperty:
class BoxProperty:
DEFAULT = ("", Style())
@@ -67,7 +67,7 @@ class Edges(NamedTuple):
yield "left", left
class _BorderProperty:
class BorderProperty:
def __set_name__(self, owner: Styles, name: str) -> None:
self._properties = (
f"{name}_top",
@@ -129,14 +129,21 @@ class _BorderProperty:
raise StyleValueError("expected 1, 2, or 4 values")
class _StyleProperty:
class StyleProperty:
DEFAULT_STYLE = Style()
def __set_name__(self, owner: Styles, name: str) -> None:
self._internal_name = f"_{name}"
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):
_style = Style.parse(style)
else:
@@ -145,7 +152,7 @@ class _StyleProperty:
return _style
class _SpacingProperty:
class SpacingProperty:
def __set_name__(self, owner: Styles, name: str) -> None:
self._internal_name = f"_{name}"
@@ -158,7 +165,7 @@ class _SpacingProperty:
return spacing
class _DocksProperty:
class DocksProperty:
def __get__(
self, obj: Styles, objtype: type[Styles] | None = None
) -> tuple[str, ...]:
@@ -173,7 +180,7 @@ class _DocksProperty:
return _docks
class _DockGroupProperty:
class DockGroupProperty:
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str:
return obj._dock_group or ""
@@ -182,7 +189,7 @@ class _DockGroupProperty:
return spacing
class _OffsetProperty:
class OffsetProperty:
def __set_name__(self, owner: Styles, name: str) -> None:
self._internal_name = f"_{name}"
@@ -195,7 +202,7 @@ class _OffsetProperty:
return offset
class _DockEdgeProperty:
class DockEdgeProperty:
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str:
return obj._dock_edge or ""
@@ -204,3 +211,37 @@ class _DockEdgeProperty:
raise ValueError(f"dock edge must be one of {friendly_list(VALID_EDGE)}")
obj._dock_edge = 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 ._style_properties import (
_BorderProperty,
_BoxProperty,
_DockEdgeProperty,
_DocksProperty,
_DockGroupProperty,
_OffsetProperty,
_SpacingProperty,
_StyleProperty,
BorderProperty,
BoxProperty,
DockEdgeProperty,
DocksProperty,
DockGroupProperty,
IntegerProperty,
OffsetProperty,
SpacingProperty,
StringProperty,
StyleProperty,
)
from .types import Display, Visibility
@@ -33,10 +35,10 @@ class Styles:
_display: Display | None = None
_visibility: Visibility | None = None
_layout: str | None = None
_text: Style = Style()
_text: Style | None = None
_layout: str = ""
_padding: Spacing | None = None
_margin: Spacing | None = None
_offset: Offset | None = None
@@ -51,71 +53,45 @@ class Styles:
_outline_bottom: 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_edge: str | None = None
_docks: tuple[str, ...] | None = None
important: set[str] = field(default_factory=set)
@property
def display(self) -> Display:
return self._display or "block"
display = StringProperty(VALID_DISPLAY, "block")
visibility = StringProperty(VALID_VISIBILITY, "visible")
layout = StringProperty(VALID_LAYOUT, "dock")
@display.setter
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
text = StyleProperty()
@property
def visibility(self) -> Visibility:
return self._visibility or "visible"
padding = SpacingProperty()
margin = SpacingProperty()
offset = OffsetProperty()
@visibility.setter
def visibility(self, visibility: Visibility) -> None:
if visibility not in VALID_VISIBILITY:
raise StyleValueError(
f"visibility must be one of {friendly_list(VALID_VISIBILITY)}"
)
self._visibility = visibility
border = BorderProperty()
border_top = BoxProperty()
border_right = BoxProperty()
border_bottom = BoxProperty()
border_left = BoxProperty()
text = _StyleProperty()
outline = BorderProperty()
outline_top = BoxProperty()
outline_right = BoxProperty()
outline_bottom = BoxProperty()
outline_left = BoxProperty()
@property
def layout(self) -> str:
return self._layout
size = IntegerProperty()
fraction = IntegerProperty()
min_size = IntegerProperty()
@layout.setter
def layout(self, layout: str) -> None:
if layout not in VALID_LAYOUT:
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()
dock_group = DockGroupProperty()
docks = DocksProperty()
dock_edge = DockEdgeProperty()
@property
def has_border(self) -> bool:
@@ -238,7 +214,6 @@ if __name__ == "__main__":
styles.docks = "foo bar"
styles.text = "italic blue"
styles.dock_group = "bar"
styles.dock_edge = "sdfsdf"
from rich import inspect, print

View File

@@ -41,10 +41,6 @@ class Dock:
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]:
for dock in self.docks:
yield from dock.widgets

View File

@@ -41,7 +41,9 @@ class View(Widget):
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.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__(
cls, layout: Callable[[], Layout] | None = None, **kwargs
@@ -144,7 +146,7 @@ class View(Widget):
for name, widget in name_widgets:
if name is not None:
widget.name = name
self.add_child(widget)
self._add_child(widget)
self.refresh()

View File

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

View File

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

View File

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

View File

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