key lines

This commit is contained in:
Will McGugan
2021-11-24 21:26:27 +00:00
parent eedb339908
commit 09544e172f
16 changed files with 177 additions and 114 deletions

View File

@@ -1,5 +1,5 @@
from textual.app import App
from textual.widgets import Placeholder
from textual.widget import Widget
class BasicApp(App):
@@ -9,35 +9,51 @@ class BasicApp(App):
App > DockView {
layout: dock;
docks: sidebar=left/1 widgets=top;
docks: side=left/1 header=top footer=bottom;
layers: base panels;
}
#sidebar {
dock-group: sidebar;
width: 40;
text: bold #09312e on #3CAEA3;
/* dock-group: header; */
width: 30;
height: 1fr;
layer: panels;
border-right: vkey #09312e;
}
#widget1 {
text: on blue;
dock-group: widgets;
height: 1fr;
#header {
text: on #173f5f;
dock-group: header;
height: 3;
border: hkey white;
}
#widget2 {
text: on red;
dock-group: widgets;
height: 1fr;
#footer {
dock-group: header;
height: 3;
border-top: hkey #0f2b41;
text: #3a3009 on #f6d55c;
}
#content {
dock-group: header;
text: on #20639B;
}
"""
async def on_mount(self) -> None:
"""Build layout here."""
await self.view.mount(
sidebar=Placeholder(), widget1=Placeholder(), widget2=Placeholder()
header=Widget(),
content=Widget(),
footer=Widget(),
sidebar=Widget(),
)
self.panic(self.view.styles)
BasicApp.run(log="textual.log")

View File

@@ -17,6 +17,8 @@ BORDER_STYLES: dict[str, tuple[str, str, str]] = {
"heavy": ("┏━┓", "┃ ┃", "┗━┛"),
"inner": ("▗▄▖", "▐ ▌", "▝▀▘"),
"outer": ("▛▀▜", "▌ ▐", "▙▄▟"),
"hkey": ("▔▔▔", " ", "▁▁▁"),
"vkey": ("▏ ▕", "▏ ▕", "▏ ▕"),
}
@@ -98,9 +100,9 @@ class Border:
render_options = options.update_dimensions(width, height)
lines = console.render_lines(self.renderable, render_options)
if len(lines) <= 2:
yield SegmentLines(lines, new_lines=True)
return
# if len(lines) <= 2:
# yield SegmentLines(lines, new_lines=True)
# return
if self.outline:
self._crop_renderable(lines, options.max_width)

View File

@@ -305,7 +305,6 @@ class App(DOMNode):
self.stylesheet.read(self.css_file)
if self.css is not None:
self.stylesheet.parse(self.css, path=f"<{self.__class__.__name__}>")
print(self.stylesheet.css)
except StylesheetParseError as error:
self.panic(error)
self._print_error_renderables()
@@ -322,16 +321,14 @@ class App(DOMNode):
# Wait for the load event to be processed, so we don't go in to application mode beforehand
await load_event.wait()
await self.post_message(events.Mount(sender=self))
view = DockView()
await self.mount(self, view)
await self.push_view(view)
driver = self._driver = self.driver_class(self.console, self)
driver.start_application_mode()
try:
mount_event = events.Mount(sender=self)
await self.dispatch_message(mount_event)
await mount_event.wait()
self.title = self._title
self.refresh()
await self.animator.start()
@@ -339,8 +336,6 @@ class App(DOMNode):
log("PROCESS END")
await self.animator.stop()
await self.close_all()
except Exception:
self.panic()
finally:
driver.stop_application_mode()
except:
@@ -376,13 +371,20 @@ class App(DOMNode):
name_widgets: Iterable[tuple[str | None, Widget]]
name_widgets = [*((None, widget) for widget in anon_widgets), *widgets.items()]
apply_stylesheet = self.stylesheet.apply
widget_events = []
for widget_id, widget in name_widgets:
if widget not in self.registry:
if widget_id is not None:
widget.id = widget_id
self._register(parent, widget)
apply_stylesheet(widget)
widget.post_message_no_wait(events.Mount(sender=parent))
mount_event = events.Mount(sender=parent)
widget_events.append((widget, mount_event))
# await widget.post_message(mount_event)
# await mount_event.wait()
for widget, event in widget_events:
widget.post_message_no_wait(event)
# await event.wait()
def is_mounted(self, widget: Widget) -> bool:
return widget in self.registry
@@ -471,7 +473,13 @@ class App(DOMNode):
async def on_event(self, event: events.Event) -> None:
# Handle input events that haven't been forwarded
# If the event has been forwaded it may have bubbled up back to the App
if isinstance(event, events.InputEvent) and not event.is_forwarded:
if isinstance(event, events.Mount):
view = DockView()
await self.mount(self, view)
await self.push_view(view)
await super().on_event(event)
elif isinstance(event, events.InputEvent) and not event.is_forwarded:
if isinstance(event, events.MouseEvent):
# Record current mouse position on App
self.mouse_position = Offset(event.x, event.y)

View File

@@ -6,7 +6,7 @@ import rich.repr
from rich.color import Color
from rich.style import Style
from .scalar import Scalar, ScalarParseError
from .scalar import get_symbols, UNIT_SYMBOL, Unit, Scalar, ScalarParseError
from ..geometry import Offset, Spacing, SpacingDimensions
from .constants import NULL_SPACING, VALID_EDGE
from .errors import StyleTypeError, StyleValueError
@@ -14,15 +14,16 @@ from ._error_tools import friendly_list
if TYPE_CHECKING:
from .styles import Styles
from .styles import DockSpecification
from .styles import DockGroup
class ScalarProperty:
def __init__(self, units: set[str]) -> None:
self.units = units
def __init__(self, units: set[Unit] | None = None) -> None:
self.units: set[Unit] = units or {*UNIT_SYMBOL}
super().__init__()
def __set_name__(self, owner: Styles, name: str) -> None:
self.name = name
self.internal_name = f"_rule_{name}"
def __get__(
@@ -38,7 +39,7 @@ class ScalarProperty:
if value is None:
new_value = None
elif isinstance(value, float):
new_value = Scalar(value, "")
new_value = Scalar(value, Unit.CELLS)
elif isinstance(value, Scalar):
new_value = value
elif isinstance(value, str):
@@ -49,7 +50,9 @@ class ScalarProperty:
else:
raise StyleValueError("expected float, Scalar, or None")
if new_value is not None and new_value.unit not in self.units:
raise StyleValueError(f"units must be one of {friendly_list(self.units)}")
raise StyleValueError(
f"{self.name} units must be one of {friendly_list(get_symbols(self.units))}"
)
setattr(obj, self.internal_name, new_value)
return value
@@ -224,12 +227,12 @@ class SpacingProperty:
class DocksProperty:
def __get__(
self, obj: Styles, objtype: type[Styles] | None = None
) -> tuple[DockSpecification, ...]:
) -> tuple[DockGroup, ...]:
return obj._rule_docks or ()
def __set__(
self, obj: Styles, docks: Iterable[DockSpecification] | None
) -> Iterable[DockSpecification] | None:
self, obj: Styles, docks: Iterable[DockGroup] | None
) -> Iterable[DockGroup] | None:
if docks is None:
obj._rule_docks = None
else:

View File

@@ -12,7 +12,7 @@ from ._error_tools import friendly_list
from ..geometry import Offset, Spacing, SpacingDimensions
from .model import Declaration
from .scalar import Scalar
from .styles import DockSpecification, Styles
from .styles import DockGroup, Styles
from .types import Edge, Display, Visibility
from .tokenize import Token
@@ -138,7 +138,7 @@ class StylesBuilder:
style_tokens: list[str] = []
append = style_tokens.append
for token in tokens:
_, _, location, token_name, value = token
token_name, value, _, _, _ = token
if token_name == "token":
if value in VALID_BORDER:
border_type = value
@@ -157,7 +157,7 @@ class StylesBuilder:
def _process_border(self, edge: str, name: str, tokens: list[Token]) -> None:
border = self._parse_border("border", tokens)
setattr(self.styles, f"_border_{edge}", border)
setattr(self.styles, f"_rule_border_{edge}", border)
def process_border(self, name: str, tokens: list[Token]) -> None:
border = self._parse_border("border", tokens)
@@ -179,7 +179,7 @@ class StylesBuilder:
def _process_outline(self, edge: str, name: str, tokens: list[Token]) -> None:
border = self._parse_border("outline", tokens)
setattr(self.styles, f"_outline_{edge}", border)
setattr(self.styles, f"_rule_outline_{edge}", border)
def process_outline(self, name: str, tokens: list[Token]) -> None:
border = self._parse_border("outline", tokens)
@@ -289,7 +289,7 @@ class StylesBuilder:
self.styles._rule_dock_group = tokens[0].value if tokens else ""
def process_docks(self, name: str, tokens: list[Token]) -> None:
docks: list[DockSpecification] = []
docks: list[DockGroup] = []
for token in tokens:
if token.name == "key_value":
key, edge_name = token.value.split("=")
@@ -308,7 +308,7 @@ class StylesBuilder:
token,
f"edge must be one of 'top', 'right', 'bottom', or 'left'; found {edge_name!r}",
)
docks.append(DockSpecification(key.strip(), cast(Edge, edge_name), z))
docks.append(DockGroup(key.strip(), cast(Edge, edge_name), z))
elif token.name == "bar":
pass
else:

View File

@@ -10,13 +10,15 @@ from ..geometry import Spacing
VALID_VISIBILITY: Final = {"visible", "hidden"}
VALID_DISPLAY: Final = {"block", "none"}
VALID_BORDER: Final = {
"rounded",
"none" "round",
"solid",
"double",
"dashed",
"heavy",
"inner",
"outer",
"hkey",
"vkey",
}
VALID_EDGE: Final = {"top", "right", "bottom", "left"}
VALID_LAYOUT: Final = {"dock", "vertical", "grid"}

View File

@@ -1,10 +1,46 @@
from __future__ import annotations
from enum import Enum, unique
import re
from typing import NamedTuple
from typing import Iterable, NamedTuple
_MATCH_SCALAR = re.compile(r"^(\d+\.?\d*)(fr|%)?$").match
@unique
class Unit(Enum):
CELLS = 1
FRACTION = 2
PERCENT = 3
WIDTH = 4
HEIGHT = 5
VIEW_WIDTH = 6
VIEW_HEIGHT = 7
UNIT_SYMBOL = {
Unit.CELLS: "",
Unit.FRACTION: "fr",
Unit.PERCENT: "%",
Unit.WIDTH: "w",
Unit.HEIGHT: "h",
Unit.VIEW_WIDTH: "vw",
Unit.VIEW_HEIGHT: "vh",
}
SYMBOL_UNIT = {v: k for k, v in UNIT_SYMBOL.items()}
_MATCH_SCALAR = re.compile(r"^(\d+\.?\d*)(fr|%|w|h|vw|vh)?$").match
def get_symbols(units: Iterable[Unit]) -> list[str]:
"""Get symbols for an iterable of units.
Args:
units (Iterable[Unit]): A number of units.
Returns:
list[str]: List of symbols.
"""
return [UNIT_SYMBOL[unit] for unit in units]
class ScalarParseError(Exception):
@@ -15,36 +51,25 @@ class Scalar(NamedTuple):
"""A numeric value and a unit."""
value: float
unit: str
unit: Unit
def __str__(self) -> str:
value, unit = self
return f"{int(value) if value.is_integer() else value}{unit}"
value, _unit = self
return f"{int(value) if value.is_integer() else value}{self.symbol}"
@property
def cells(self) -> int | None:
value, unit = self
if unit:
return None
else:
return int(value)
return int(value) if unit == Unit.CELLS else None
@property
def fraction(self) -> int | None:
value, unit = self
if unit == "fr":
return int(value)
else:
return None
return int(value) if unit == Unit.FRACTION else None
def resolve_size(self, total: int, total_fraction: int) -> int:
value, unit = self
if unit == "":
return int(value)
elif unit == "%":
return int(total * value / 100.0)
else: # if unit == "fr":
return int((value / total_fraction) * total)
@property
def symbol(self) -> str:
return UNIT_SYMBOL[self.unit]
@classmethod
def parse(cls, token: str) -> Scalar:
@@ -62,11 +87,14 @@ class Scalar(NamedTuple):
match = _MATCH_SCALAR(token)
if match is None:
raise ScalarParseError(f"{token!r} is not a valid scalar")
value, unit = match.groups()
scalar = cls(float(value), unit or "")
value, unit_name = match.groups()
scalar = cls(float(value), SYMBOL_UNIT[unit_name or ""])
return scalar
if __name__ == "__main__":
print(Scalar.parse("3.14"))
print(Scalar.parse("3.14fr"))
s = Scalar.parse("23")
print(repr(s))
print(repr(s.cells))

View File

@@ -43,7 +43,7 @@ else:
from typing_extensions import Literal
class DockSpecification(NamedTuple):
class DockGroup(NamedTuple):
name: str
edge: Edge
z: int
@@ -83,7 +83,7 @@ class Styles:
_rule_layout: str | None = None
_rule_dock_group: str | None = None
_rule_docks: tuple[DockSpecification, ...] | None = None
_rule_docks: tuple[DockGroup, ...] | None = None
_rule_layers: tuple[str, ...] | None = None
_rule_layer: str | None = None
@@ -115,10 +115,10 @@ class Styles:
outline_bottom = BoxProperty()
outline_left = BoxProperty()
width = ScalarProperty({"", "fr"})
height = ScalarProperty({"", "fr"})
min_width = ScalarProperty({"", "fr"})
min_height = ScalarProperty({"", "fr"})
width = ScalarProperty()
height = ScalarProperty()
min_width = ScalarProperty()
min_height = ScalarProperty()
dock_group = DockGroupProperty()
docks = DocksProperty()
@@ -251,8 +251,8 @@ class Styles:
append_declaration(
"docks",
" ".join(
(f"{key}={value}/{z}" if z else f"{key}={value}")
for key, value, z in self._rule_docks
(f"{name}={edge}/{z}" if z else f"{name}={edge}")
for name, edge, z in self._rule_docks
),
)
if self._rule_layers is not None:

View File

@@ -134,7 +134,6 @@ class Stylesheet:
for name, specificity_rules in rule_attributes.items()
]
node.styles.apply_rules(node_rules)
log(node, node_rules)
if __name__ == "__main__":

View File

@@ -46,7 +46,7 @@ expect_declaration_content = Expect(
comment_start=r"\/\*",
percentage=r"\d+\%",
scalar=r"\d+\.?\d*(?:fr|%)?",
color=r"\#[0-9a-f]{6}|color\([0-9]{1,3}\)|rgb\(\d{1,3}\,\s?\d{1,3}\,\s?\d{1,3}\)",
color=r"\#[0-9a-fA-F]{6}|color\([0-9]{1,3}\)|rgb\(\d{1,3}\,\s?\d{1,3}\,\s?\d{1,3}\)",
key_value=r"[a-zA-Z_-][a-zA-Z0-9_-]*=[0-9a-zA-Z_\-\/]+",
token="[a-zA-Z_-]+",
string=r"\".*?\"",

View File

@@ -67,7 +67,6 @@ class DockLayout(Layout):
self, view: View, size: Size, scroll: Offset
) -> Iterable[WidgetPlacement]:
map: LayoutMap = LayoutMap(size)
width, height = size
layout_region = Region(0, 0, width, height)
layers: dict[int, Region] = defaultdict(lambda: layout_region)
@@ -170,5 +169,3 @@ class DockLayout(Layout):
region = Region(x, y, width - total, height)
layers[z] = region
return map

View File

@@ -52,6 +52,9 @@ class Message:
cls.bubble = bubble
cls.verbosity = verbosity
def set_done(self) -> None:
self._done_event.set()
@property
def _done_event(self) -> Event:
if self.__done_event is None:

View File

@@ -225,14 +225,11 @@ class MessagePump:
async def dispatch_message(self, message: Message) -> bool | None:
_rich_traceback_guard = True
try:
if isinstance(message, events.Event):
if not isinstance(message, events.Null):
await self.on_event(message)
else:
return await self.on_message(message)
finally:
message._done_event.set()
if isinstance(message, events.Event):
if not isinstance(message, events.Null):
await self.on_event(message)
else:
return await self.on_message(message)
return False
def _get_dispatch_methods(
@@ -248,9 +245,12 @@ class MessagePump:
async def on_event(self, event: events.Event) -> None:
_rich_traceback_guard = True
for method in self._get_dispatch_methods(f"on_{event.name}", event):
log(event, ">>>", self, verbosity=event.verbosity)
await invoke(method, event)
try:
for method in self._get_dispatch_methods(f"on_{event.name}", event):
log(event, ">>>", self, verbosity=event.verbosity)
await invoke(method, event)
finally:
event.set_done()
if event.bubble and self._parent and not event._stop_propagation:
if event.sender == self._parent:
@@ -264,9 +264,12 @@ class MessagePump:
method_name = f"handle_{message.name}"
method = getattr(self, method_name, None)
if method is not None:
log(message, ">>>", self, verbosity=message.verbosity)
await invoke(method, message)
try:
if method is not None:
log(message, ">>>", self, verbosity=message.verbosity)
await invoke(method, message)
finally:
message.set_done()
if message.bubble and self._parent and not message._stop_propagation:
if message.sender == self._parent:

View File

@@ -118,6 +118,7 @@ class View(Widget):
if cached_size == size and cached_scroll == scroll:
return arrangement
arrangement = list(self._layout.arrange(self, size, scroll))
self.log(arrangement)
self._cached_arrangement = (size, scroll, arrangement)
return arrangement

View File

@@ -19,7 +19,7 @@ from rich.padding import Padding
from rich.pretty import Pretty
from rich.style import Style
from rich.styled import Styled
from rich.text import TextType
from rich.text import Text, TextType
from . import events
from . import errors
@@ -135,24 +135,28 @@ class Widget(DOMNode):
Returns:
RenderableType: A new renderable.
"""
renderable = Styled(self.render(), self.styles.text)
renderable = self.render()
styles = self.styles
if self.padding is not None:
renderable = Padding(renderable, self.padding)
if self.border not in ("", "none"):
_border_style = self.console.get_style(self.border_style)
renderable = Border(
renderable,
(
("heavy", _border_style),
("heavy", _border_style),
("heavy", _border_style),
("heavy", _border_style),
),
)
if styles.has_border:
renderable = Border(renderable, styles.border)
# _border_style = self.console.get_style(self.border_style)
# renderable = Border(
# renderable,
# (
# ("heavy", _border_style),
# ("heavy", _border_style),
# ("heavy", _border_style),
# ("heavy", _border_style),
# ),
# )
if self.margin is not None:
renderable = Padding(renderable, self.margin)
if self.style:
renderable = Styled(renderable, self.style)
renderable = Styled(renderable, styles.text)
return renderable
@property
@@ -269,9 +273,7 @@ class Widget(DOMNode):
Returns:
RenderableType: Any renderable
"""
return Panel(
Align.center(Pretty(self), vertical="middle"), title=self.__class__.__name__
)
return Align.center(Text(f"#{self.id}"), vertical="middle")
async def action(self, action: str, *params) -> None:
await self.app.action(action, self)

View File

@@ -10,7 +10,6 @@ import rich.repr
from logging import getLogger
from .. import events
from ..geometry import Offset
from ..widget import Reactive, Widget
log = getLogger("rich")