mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
dock
This commit is contained in:
@@ -289,6 +289,7 @@ class App(DOMNode):
|
|||||||
def _print_error_renderables(self) -> None:
|
def _print_error_renderables(self) -> None:
|
||||||
for renderable in self._exit_renderables:
|
for renderable in self._exit_renderables:
|
||||||
self.error_console.print(renderable)
|
self.error_console.print(renderable)
|
||||||
|
self._exit_renderables.clear()
|
||||||
|
|
||||||
async def process_messages(self) -> None:
|
async def process_messages(self) -> None:
|
||||||
active_app.set(self)
|
active_app.set(self)
|
||||||
@@ -300,7 +301,6 @@ class App(DOMNode):
|
|||||||
self.stylesheet.read(self.css_file)
|
self.stylesheet.read(self.css_file)
|
||||||
if self.css is not None:
|
if self.css is not None:
|
||||||
self.stylesheet.parse(self.css, path=f"<{self.__class__.__name__}>")
|
self.stylesheet.parse(self.css, path=f"<{self.__class__.__name__}>")
|
||||||
print(self.stylesheet.css)
|
|
||||||
except StylesheetParseError as error:
|
except StylesheetParseError as error:
|
||||||
self.panic(error)
|
self.panic(error)
|
||||||
self._print_error_renderables()
|
self._print_error_renderables()
|
||||||
@@ -310,20 +310,18 @@ class App(DOMNode):
|
|||||||
self._print_error_renderables()
|
self._print_error_renderables()
|
||||||
return
|
return
|
||||||
|
|
||||||
load_event = events.Load(sender=self)
|
|
||||||
await self.dispatch_message(load_event)
|
|
||||||
await self.post_message(events.Mount(self))
|
|
||||||
await self.push_view(DockView())
|
|
||||||
|
|
||||||
# Wait for the load event to be processed, so we don't go in to application mode beforehand
|
|
||||||
await load_event.wait()
|
|
||||||
|
|
||||||
driver = self._driver = self.driver_class(self.console, self)
|
|
||||||
try:
|
try:
|
||||||
|
load_event = events.Load(sender=self)
|
||||||
|
await self.dispatch_message(load_event)
|
||||||
|
await self.post_message(events.Mount(self))
|
||||||
|
await self.push_view(DockView())
|
||||||
|
|
||||||
|
# Wait for the load event to be processed, so we don't go in to application mode beforehand
|
||||||
|
await load_event.wait()
|
||||||
|
|
||||||
|
driver = self._driver = self.driver_class(self.console, self)
|
||||||
|
|
||||||
driver.start_application_mode()
|
driver.start_application_mode()
|
||||||
except Exception:
|
|
||||||
self.console.print_exception()
|
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
self.title = self._title
|
self.title = self._title
|
||||||
self.refresh()
|
self.refresh()
|
||||||
@@ -332,15 +330,17 @@ class App(DOMNode):
|
|||||||
log("PROCESS END")
|
log("PROCESS END")
|
||||||
await self.animator.stop()
|
await self.animator.stop()
|
||||||
await self.close_all()
|
await self.close_all()
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.panic()
|
self.panic()
|
||||||
finally:
|
finally:
|
||||||
driver.stop_application_mode()
|
driver.stop_application_mode()
|
||||||
if self._exit_renderables:
|
except:
|
||||||
self._print_error_renderables()
|
self.panic()
|
||||||
if self.log_file is not None:
|
finally:
|
||||||
self.log_file.close()
|
if self._exit_renderables:
|
||||||
|
self._print_error_renderables()
|
||||||
|
if self.log_file is not None:
|
||||||
|
self.log_file.close()
|
||||||
|
|
||||||
def register(self, child: MessagePump, parent: MessagePump) -> bool:
|
def register(self, child: MessagePump, parent: MessagePump) -> bool:
|
||||||
if child not in self.registry:
|
if child not in self.registry:
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class ScalarProperty:
|
class ScalarProperty:
|
||||||
|
def __init__(self, units: set[str]) -> None:
|
||||||
|
self.units = units
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||||
self.internal_name = f"_rule_{name}"
|
self.internal_name = f"_rule_{name}"
|
||||||
|
|
||||||
@@ -27,19 +31,23 @@ class ScalarProperty:
|
|||||||
def __set__(
|
def __set__(
|
||||||
self, obj: Styles, value: float | Scalar | str | None
|
self, obj: Styles, value: float | Scalar | str | None
|
||||||
) -> float | Scalar | str | None:
|
) -> float | Scalar | str | None:
|
||||||
|
new_value: Scalar | None = None
|
||||||
if value is None:
|
if value is None:
|
||||||
setattr(obj, self.internal_name, None)
|
new_value = None
|
||||||
elif isinstance(value, float):
|
elif isinstance(value, float):
|
||||||
setattr(obj, self.internal_name, Scalar(value, "cells"))
|
new_value = Scalar(value, "")
|
||||||
elif isinstance(value, Scalar):
|
elif isinstance(value, Scalar):
|
||||||
setattr(obj, self.internal_name, value)
|
new_value = value
|
||||||
elif isinstance(value, str):
|
elif isinstance(value, str):
|
||||||
try:
|
try:
|
||||||
setattr(obj, self.internal_name, Scalar.parse(value))
|
new_value = Scalar.parse(value)
|
||||||
except ScalarParseError:
|
except ScalarParseError:
|
||||||
raise StyleValueError("unable to parse scalar from {value!r}")
|
raise StyleValueError("unable to parse scalar from {value!r}")
|
||||||
else:
|
else:
|
||||||
raise StyleValueError("expected float, Scalar, or None")
|
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)}")
|
||||||
|
setattr(obj, self.internal_name, new_value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import rich.repr
|
|||||||
from rich.color import ANSI_COLOR_NAMES, Color
|
from rich.color import ANSI_COLOR_NAMES, Color
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
from .constants import VALID_BORDER, VALID_DISPLAY, VALID_VISIBILITY
|
from .constants import VALID_BORDER, VALID_EDGE, VALID_DISPLAY, VALID_VISIBILITY
|
||||||
from .errors import DeclarationError, StyleValueError
|
from .errors import DeclarationError, StyleValueError
|
||||||
from ._error_tools import friendly_list
|
from ._error_tools import friendly_list
|
||||||
from ..geometry import Offset, Spacing, SpacingDimensions
|
from ..geometry import Offset, Spacing, SpacingDimensions
|
||||||
@@ -75,7 +75,7 @@ class StylesBuilder:
|
|||||||
if not tokens:
|
if not tokens:
|
||||||
return
|
return
|
||||||
if len(tokens) == 1:
|
if len(tokens) == 1:
|
||||||
setattr(self.styles, f"_rule_{name}", Scalar.parse(tokens[0].value))
|
setattr(self.styles, name, Scalar.parse(tokens[0].value))
|
||||||
else:
|
else:
|
||||||
self.error(name, tokens[0], "a single scalar is expected")
|
self.error(name, tokens[0], "a single scalar is expected")
|
||||||
|
|
||||||
@@ -294,8 +294,15 @@ class StylesBuilder:
|
|||||||
if token.name == "token":
|
if token.name == "token":
|
||||||
docks.append((token.value, ""))
|
docks.append((token.value, ""))
|
||||||
elif token.name == "key_value":
|
elif token.name == "key_value":
|
||||||
key, value = token.value.split("=")
|
key, group_name = token.value.split("=")
|
||||||
docks.append((key.strip(), value.strip()))
|
group_name = group_name.strip().lower()
|
||||||
|
if group_name not in VALID_EDGE:
|
||||||
|
self.error(
|
||||||
|
name,
|
||||||
|
token,
|
||||||
|
f"edge must be one of 'top', 'right', 'bottom', or 'left'; found {group_name!r}",
|
||||||
|
)
|
||||||
|
docks.append((key.strip(), group_name))
|
||||||
elif token.name == "bar":
|
elif token.name == "bar":
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -21,6 +21,31 @@ class Scalar(NamedTuple):
|
|||||||
value, unit = self
|
value, unit = self
|
||||||
return f"{int(value) if value.is_integer() else value}{unit}"
|
return f"{int(value) if value.is_integer() else value}{unit}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cells(self) -> int | None:
|
||||||
|
value, unit = self
|
||||||
|
if unit:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fraction(self) -> int | None:
|
||||||
|
value, unit = self
|
||||||
|
if unit == "fr":
|
||||||
|
return int(value)
|
||||||
|
else:
|
||||||
|
return 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)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse(cls, token: str) -> Scalar:
|
def parse(cls, token: str) -> Scalar:
|
||||||
"""Parse a string in to a Scalar
|
"""Parse a string in to a Scalar
|
||||||
|
|||||||
@@ -103,10 +103,10 @@ class Styles:
|
|||||||
outline_bottom = BoxProperty()
|
outline_bottom = BoxProperty()
|
||||||
outline_left = BoxProperty()
|
outline_left = BoxProperty()
|
||||||
|
|
||||||
width = ScalarProperty()
|
width = ScalarProperty({"", "fr"})
|
||||||
height = ScalarProperty()
|
height = ScalarProperty({"", "fr"})
|
||||||
min_width = ScalarProperty()
|
min_width = ScalarProperty({"", "fr"})
|
||||||
min_height = ScalarProperty()
|
min_height = ScalarProperty({"", "fr"})
|
||||||
|
|
||||||
dock_group = DockGroupProperty()
|
dock_group = DockGroupProperty()
|
||||||
docks = DocksProperty()
|
docks = DocksProperty()
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ class DOMNode(MessagePump):
|
|||||||
append(node)
|
append(node)
|
||||||
return result[::-1]
|
return result[::-1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def visible(self) -> bool:
|
||||||
|
return self.styles.display != "none"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tree(self) -> Tree:
|
def tree(self) -> Tree:
|
||||||
highlighter = ReprHighlighter()
|
highlighter = ReprHighlighter()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from rich.segment import Segment, SegmentLines
|
|||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
from . import log, panic
|
from . import log, panic
|
||||||
|
from .dom import DOMNode
|
||||||
from ._loop import loop_last
|
from ._loop import loop_last
|
||||||
from .layout_map import LayoutMap
|
from .layout_map import LayoutMap
|
||||||
from ._profile import timer
|
from ._profile import timer
|
||||||
@@ -149,7 +150,7 @@ class Layout(ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_widgets(self) -> Iterable[Widget]:
|
def get_widgets(self, view: View) -> Iterable[DOMNode]:
|
||||||
...
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -168,7 +169,7 @@ class Layout(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
async def mount_all(self, view: "View") -> None:
|
async def mount_all(self, view: "View") -> None:
|
||||||
await view.mount(*self.get_widgets())
|
await view.mount(*self.get_widgets(view))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def map(self) -> LayoutMap | None:
|
def map(self) -> LayoutMap | None:
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ from collections import defaultdict
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Iterable, TYPE_CHECKING, Sequence
|
from typing import Iterable, TYPE_CHECKING, Sequence
|
||||||
|
|
||||||
from rich.console import Console
|
from ..dom import DOMNode
|
||||||
|
|
||||||
from .._layout_resolve import layout_resolve
|
from .._layout_resolve import layout_resolve
|
||||||
from ..geometry import Offset, Region, Size
|
from ..geometry import Offset, Region, Size
|
||||||
from ..layout import Layout, WidgetPlacement
|
from ..layout import Layout, WidgetPlacement
|
||||||
@@ -35,14 +34,28 @@ class DockOptions:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Dock:
|
class Dock:
|
||||||
edge: DockEdge
|
edge: str
|
||||||
widgets: Sequence[Widget]
|
widgets: Sequence[DOMNode]
|
||||||
z: int = 0
|
z: int = 0
|
||||||
|
|
||||||
|
|
||||||
class DockLayout(Layout):
|
class DockLayout(Layout):
|
||||||
def get_widgets(self) -> Iterable[Widget]:
|
def __init__(self) -> None:
|
||||||
for dock in self.docks:
|
super().__init__()
|
||||||
|
self._docks: list[Dock] | None = None
|
||||||
|
|
||||||
|
def get_docks(self, view: View) -> list[Dock]:
|
||||||
|
groups: dict[str, list[DOMNode]] = defaultdict(list)
|
||||||
|
for child in view.children:
|
||||||
|
groups[child.styles.dock_group].append(child)
|
||||||
|
docks: list[Dock] = []
|
||||||
|
append_dock = docks.append
|
||||||
|
for name, edge in view.styles.docks:
|
||||||
|
append_dock(Dock(edge, groups[name], 0))
|
||||||
|
return docks
|
||||||
|
|
||||||
|
def get_widgets(self, view: View) -> Iterable[DOMNode]:
|
||||||
|
for dock in self.get_docks(view):
|
||||||
yield from dock.widgets
|
yield from dock.widgets
|
||||||
|
|
||||||
def arrange(
|
def arrange(
|
||||||
@@ -54,10 +67,23 @@ class DockLayout(Layout):
|
|||||||
layout_region = Region(0, 0, width, height)
|
layout_region = Region(0, 0, width, height)
|
||||||
layers: dict[int, Region] = defaultdict(lambda: layout_region)
|
layers: dict[int, Region] = defaultdict(lambda: layout_region)
|
||||||
|
|
||||||
for index, dock in enumerate(self.docks):
|
docks = self.get_docks(view)
|
||||||
|
|
||||||
|
for index, dock in enumerate(docks):
|
||||||
|
|
||||||
dock_options = [
|
dock_options = [
|
||||||
DockOptions(
|
(
|
||||||
widget.layout_size, widget.layout_fraction, widget.layout_min_size
|
DockOptions(
|
||||||
|
widget.styles.width.cells,
|
||||||
|
widget.styles.width.fraction or 1,
|
||||||
|
widget.styles.min_width.cells or 1,
|
||||||
|
)
|
||||||
|
if dock.edge in ("left", "right")
|
||||||
|
else DockOptions(
|
||||||
|
widget.styles.height.cells,
|
||||||
|
widget.styles.height.fraction or 1,
|
||||||
|
widget.styles.min_height.cells or 1,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
for widget in dock.widgets
|
for widget in dock.widgets
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user