mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
refactor of compositor
This commit is contained in:
@@ -17,7 +17,7 @@ class Clock(Widget):
|
|||||||
|
|
||||||
class ClockApp(App):
|
class ClockApp(App):
|
||||||
async def on_mount(self):
|
async def on_mount(self):
|
||||||
await self.view.dock(Clock())
|
await self.screen.dock(Clock())
|
||||||
|
|
||||||
|
|
||||||
ClockApp.run()
|
ClockApp.run()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class HoverApp(App):
|
|||||||
"""Build layout here."""
|
"""Build layout here."""
|
||||||
|
|
||||||
hovers = (Hover() for _ in range(10))
|
hovers = (Hover() for _ in range(10))
|
||||||
await self.view.dock(*hovers, edge="top")
|
await self.screen.dock(*hovers, edge="top")
|
||||||
|
|
||||||
|
|
||||||
HoverApp.run(log="textual.log")
|
HoverApp.run(log="textual.log")
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ class SimpleApp(App):
|
|||||||
async def on_mount(self) -> None:
|
async def on_mount(self) -> None:
|
||||||
"""Build layout here."""
|
"""Build layout here."""
|
||||||
|
|
||||||
await self.view.dock(Placeholder(), edge="left", size=40)
|
await self.screen.dock(Placeholder(), edge="left", size=40)
|
||||||
await self.view.dock(Placeholder(), Placeholder(), edge="top")
|
await self.screen.dock(Placeholder(), Placeholder(), edge="top")
|
||||||
|
|
||||||
|
|
||||||
SimpleApp.run(log="textual.log")
|
SimpleApp.run(log="textual.log")
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ class SmoothApp(App):
|
|||||||
footer = Footer()
|
footer = Footer()
|
||||||
self.bar = Placeholder(name="left")
|
self.bar = Placeholder(name="left")
|
||||||
|
|
||||||
await self.view.dock(footer, edge="bottom")
|
await self.screen.dock(footer, edge="bottom")
|
||||||
await self.view.dock(Placeholder(), Placeholder(), edge="top")
|
await self.screen.dock(Placeholder(), Placeholder(), edge="top")
|
||||||
await self.view.dock(self.bar, edge="left", size=40, z=1)
|
await self.screen.dock(self.bar, edge="left", size=40, z=1)
|
||||||
|
|
||||||
self.bar.layout_offset_x = -40
|
self.bar.layout_offset_x = -40
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
$primary: #20639b;
|
$primary: #20639b;
|
||||||
|
|
||||||
App > View {
|
App > Screen {
|
||||||
layout: dock;
|
layout: dock;
|
||||||
docks: side=left/1;
|
docks: side=left/1;
|
||||||
text: on $primary;
|
text: on $primary;
|
||||||
@@ -13,6 +13,7 @@ App > View {
|
|||||||
dock: side;
|
dock: side;
|
||||||
width: 30;
|
width: 30;
|
||||||
offset-x: -100%;
|
offset-x: -100%;
|
||||||
|
|
||||||
transition: offset 500ms in_out_cubic;
|
transition: offset 500ms in_out_cubic;
|
||||||
border-right: outer #09312e;
|
border-right: outer #09312e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class MyApp(App):
|
|||||||
|
|
||||||
self.body = body = ScrollView(auto_width=True)
|
self.body = body = ScrollView(auto_width=True)
|
||||||
|
|
||||||
await self.view.dock(body)
|
await self.screen.dock(body)
|
||||||
|
|
||||||
async def add_content():
|
async def add_content():
|
||||||
table = Table(title="Demo")
|
table = Table(title="Demo")
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ class CalculatorApp(App):
|
|||||||
|
|
||||||
async def on_mount(self) -> None:
|
async def on_mount(self) -> None:
|
||||||
"""Mount the calculator widget."""
|
"""Mount the calculator widget."""
|
||||||
await self.view.dock(Calculator())
|
await self.screen.dock(Calculator())
|
||||||
|
|
||||||
|
|
||||||
CalculatorApp.run(title="Calculator Test", log="textual.log")
|
CalculatorApp.run(title="Calculator Test", log="textual.log")
|
||||||
|
|||||||
@@ -36,14 +36,14 @@ class MyApp(App):
|
|||||||
self.directory = DirectoryTree(self.path, "Code")
|
self.directory = DirectoryTree(self.path, "Code")
|
||||||
|
|
||||||
# Dock our widgets
|
# Dock our widgets
|
||||||
await self.view.dock(Header(), edge="top")
|
await self.screen.dock(Header(), edge="top")
|
||||||
await self.view.dock(Footer(), edge="bottom")
|
await self.screen.dock(Footer(), edge="bottom")
|
||||||
|
|
||||||
# Note the directory is also in a scroll view
|
# Note the directory is also in a scroll view
|
||||||
await self.view.dock(
|
await self.screen.dock(
|
||||||
ScrollView(self.directory), edge="left", size=48, name="sidebar"
|
ScrollView(self.directory), edge="left", size=48, name="sidebar"
|
||||||
)
|
)
|
||||||
await self.view.dock(self.body, edge="top")
|
await self.screen.dock(self.body, edge="top")
|
||||||
|
|
||||||
async def handle_file_click(self, message: FileClick) -> None:
|
async def handle_file_click(self, message: FileClick) -> None:
|
||||||
"""A message sent by the directory tree when a file is clicked."""
|
"""A message sent by the directory tree when a file is clicked."""
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ class EasingApp(App):
|
|||||||
await tree.add(tree.root.id, easing_key, {"easing": easing_key})
|
await tree.add(tree.root.id, easing_key, {"easing": easing_key})
|
||||||
await tree.root.expand()
|
await tree.root.expand()
|
||||||
|
|
||||||
await self.view.dock(ScrollView(tree), edge="left", size=32)
|
await self.screen.dock(ScrollView(tree), edge="left", size=32)
|
||||||
await self.view.dock(self.easing_view)
|
await self.screen.dock(self.easing_view)
|
||||||
await self.easing_view.dock(self.placeholder, edge="left", size=32)
|
await self.easing_view.dock(self.placeholder, edge="left", size=32)
|
||||||
|
|
||||||
async def handle_tree_click(self, message: TreeClick[dict]) -> None:
|
async def handle_tree_click(self, message: TreeClick[dict]) -> None:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class GridTest(App):
|
|||||||
async def on_mount(self) -> None:
|
async def on_mount(self) -> None:
|
||||||
"""Make a simple grid arrangement."""
|
"""Make a simple grid arrangement."""
|
||||||
|
|
||||||
grid = await self.view.dock_grid(edge="left", name="left")
|
grid = await self.screen.dock_grid(edge="left", name="left")
|
||||||
|
|
||||||
grid.add_column(fraction=1, name="left", min_size=20)
|
grid.add_column(fraction=1, name="left", min_size=20)
|
||||||
grid.add_column(size=30, name="center")
|
grid.add_column(size=30, name="center")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class GridTest(App):
|
|||||||
async def on_mount(self, event: events.Mount) -> None:
|
async def on_mount(self, event: events.Mount) -> None:
|
||||||
"""Create a grid with auto-arranging cells."""
|
"""Create a grid with auto-arranging cells."""
|
||||||
|
|
||||||
grid = await self.view.dock_grid()
|
grid = await self.screen.dock_grid()
|
||||||
|
|
||||||
grid.add_column("col", fraction=1, max_size=20)
|
grid.add_column("col", fraction=1, max_size=20)
|
||||||
grid.add_row("row", fraction=1, max_size=10)
|
grid.add_row("row", fraction=1, max_size=10)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class MyApp(App):
|
|||||||
"""Create and dock the widgets."""
|
"""Create and dock the widgets."""
|
||||||
|
|
||||||
body = ScrollView()
|
body = ScrollView()
|
||||||
await self.view.mount(
|
await self.screen.mount(
|
||||||
Header(),
|
Header(),
|
||||||
Footer(),
|
Footer(),
|
||||||
body=body,
|
body=body,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class BasicApp(App):
|
|||||||
self.bind("a", "toggle_class('#header', '-visible')")
|
self.bind("a", "toggle_class('#header', '-visible')")
|
||||||
self.bind("c", "toggle_class('#content', '-content-visible')")
|
self.bind("c", "toggle_class('#content', '-content-visible')")
|
||||||
self.bind("d", "toggle_class('#footer', 'dim')")
|
self.bind("d", "toggle_class('#footer', 'dim')")
|
||||||
|
self.bind("x", "dump")
|
||||||
|
|
||||||
def on_mount(self):
|
def on_mount(self):
|
||||||
"""Build layout here."""
|
"""Build layout here."""
|
||||||
@@ -29,5 +30,8 @@ class BasicApp(App):
|
|||||||
sidebar=Widget(),
|
sidebar=Widget(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def action_dump(self):
|
||||||
|
self.panic(self.tree)
|
||||||
|
|
||||||
|
|
||||||
BasicApp.run(css_file="dev_sandbox.scss", watch_css=True, log="textual.log")
|
BasicApp.run(css_file="dev_sandbox.scss", watch_css=True, log="textual.log")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
$text: #f0f0f0;
|
$text: #f0f0f0;
|
||||||
$primary: #021720;
|
$primary: #021720;
|
||||||
$secondary:#95d52a;
|
$secondary: #95d52a;
|
||||||
$background: #262626;
|
$background: #262626;
|
||||||
|
|
||||||
$primary-style: $text on $background;
|
$primary-style: $text on $background;
|
||||||
|
|||||||
@@ -1,4 +1,24 @@
|
|||||||
#uber {
|
#uber {
|
||||||
border: heavy green;
|
/* border: heavy green; */
|
||||||
margin: 5;
|
margin: 2;
|
||||||
|
layout: dock;
|
||||||
|
docks: panels=top;
|
||||||
|
}
|
||||||
|
|
||||||
|
#child1 {
|
||||||
|
dock: panels;
|
||||||
|
}
|
||||||
|
|
||||||
|
#child2 {
|
||||||
|
dock: panels;
|
||||||
|
}
|
||||||
|
|
||||||
|
#child3 {
|
||||||
|
dock: panels;
|
||||||
|
}
|
||||||
|
|
||||||
|
#uber2 {
|
||||||
|
margin: 3;
|
||||||
|
layout: dock;
|
||||||
|
docks: _default=left;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from tkinter import Place
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
from textual import events
|
from textual import events
|
||||||
from textual.widgets import Placeholder
|
from textual.widgets import Placeholder
|
||||||
@@ -9,7 +10,22 @@ class BasicApp(App):
|
|||||||
|
|
||||||
def on_mount(self):
|
def on_mount(self):
|
||||||
"""Build layout here."""
|
"""Build layout here."""
|
||||||
self.mount(uber=Placeholder())
|
|
||||||
|
uber2 = Widget()
|
||||||
|
uber2.add_children(
|
||||||
|
Placeholder(id="uber2-child1"),
|
||||||
|
Placeholder(id="uber2-child2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.mount(
|
||||||
|
uber=Widget(
|
||||||
|
Placeholder(id="child1"),
|
||||||
|
Placeholder(id="child2"),
|
||||||
|
Placeholder(id="child3"),
|
||||||
|
),
|
||||||
|
uber2=uber2,
|
||||||
|
)
|
||||||
|
# self.panic(self.tree)
|
||||||
|
|
||||||
async def on_key(self, event: events.Key) -> None:
|
async def on_key(self, event: events.Key) -> None:
|
||||||
await self.dispatch_key(event)
|
await self.dispatch_key(event)
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ class BoundAnimator:
|
|||||||
|
|
||||||
|
|
||||||
class Animator:
|
class Animator:
|
||||||
|
"""An object to manage updates to a given attributed over a period of time."""
|
||||||
|
|
||||||
def __init__(self, target: MessageTarget, frames_per_second: int = 60) -> None:
|
def __init__(self, target: MessageTarget, frames_per_second: int = 60) -> None:
|
||||||
self._animations: dict[tuple[object, str], Animation] = {}
|
self._animations: dict[tuple[object, str], Animation] = {}
|
||||||
self.target = target
|
self.target = target
|
||||||
@@ -225,4 +227,6 @@ class Animator:
|
|||||||
|
|
||||||
def on_animation_frame(self) -> None:
|
def on_animation_frame(self) -> None:
|
||||||
# TODO: We should be able to do animation without refreshing everything
|
# TODO: We should be able to do animation without refreshing everything
|
||||||
self.target.view.refresh(True, True)
|
|
||||||
|
self.target.screen.refresh(layout=True)
|
||||||
|
# self.target.screen.app.refresh()
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ from rich.control import Control
|
|||||||
from rich.segment import Segment, SegmentLines
|
from rich.segment import Segment, SegmentLines
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
from . import log
|
from . import errors, log
|
||||||
from .geometry import Region, Offset, Size
|
from .geometry import Region, Offset, Size
|
||||||
|
|
||||||
from .layout import WidgetPlacement
|
|
||||||
from ._loop import loop_last
|
from ._loop import loop_last
|
||||||
from ._types import Lines
|
from ._types import Lines
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
@@ -26,13 +25,10 @@ else: # pragma: no cover
|
|||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .screen import Screen
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
|
|
||||||
|
|
||||||
class NoWidget(Exception):
|
|
||||||
"""Raised when there is no widget at the requested coordinate."""
|
|
||||||
|
|
||||||
|
|
||||||
class ReflowResult(NamedTuple):
|
class ReflowResult(NamedTuple):
|
||||||
"""The result of a reflow operation. Describes the chances to widgets."""
|
"""The result of a reflow operation. Describes the chances to widgets."""
|
||||||
|
|
||||||
@@ -137,8 +133,8 @@ class Compositor:
|
|||||||
self.width = size.width
|
self.width = size.width
|
||||||
self.height = size.height
|
self.height = size.height
|
||||||
|
|
||||||
map, virtual_size = self._arrange_root(parent)
|
map, virtual_size, widgets = self._arrange_root(parent)
|
||||||
log(map)
|
|
||||||
self._require_update = False
|
self._require_update = False
|
||||||
|
|
||||||
old_widgets = set(self.map.keys())
|
old_widgets = set(self.map.keys())
|
||||||
@@ -165,12 +161,13 @@ class Compositor:
|
|||||||
}
|
}
|
||||||
|
|
||||||
parent.virtual_size = virtual_size
|
parent.virtual_size = virtual_size
|
||||||
|
self.widgets.clear()
|
||||||
|
self.widgets.update(widgets)
|
||||||
return ReflowResult(
|
return ReflowResult(
|
||||||
hidden=hidden_widgets, shown=shown_widgets, resized=resized_widgets
|
hidden=hidden_widgets, shown=shown_widgets, resized=resized_widgets
|
||||||
)
|
)
|
||||||
|
|
||||||
def _arrange_root(self, root: Widget) -> tuple[RenderRegionMap, Size]:
|
def _arrange_root(self, root: Widget) -> tuple[RenderRegionMap, Size, set[Widget]]:
|
||||||
"""Arrange a widgets children based on its layout attribute.
|
"""Arrange a widgets children based on its layout attribute.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -192,7 +189,7 @@ class Compositor:
|
|||||||
order: tuple[int, ...],
|
order: tuple[int, ...],
|
||||||
clip: Region,
|
clip: Region,
|
||||||
):
|
):
|
||||||
widgets: set[Widget] = set()
|
widgets.add(widget)
|
||||||
styles_offset = widget.styles.offset
|
styles_offset = widget.styles.offset
|
||||||
total_region = region
|
total_region = region
|
||||||
layout_offset = (
|
layout_offset = (
|
||||||
@@ -200,7 +197,6 @@ class Compositor:
|
|||||||
if styles_offset
|
if styles_offset
|
||||||
else ORIGIN
|
else ORIGIN
|
||||||
)
|
)
|
||||||
|
|
||||||
map[widget] = RenderRegion(region + layout_offset, order, clip)
|
map[widget] = RenderRegion(region + layout_offset, order, clip)
|
||||||
|
|
||||||
if widget.layout is not None:
|
if widget.layout is not None:
|
||||||
@@ -227,12 +223,10 @@ class Compositor:
|
|||||||
return total_region.size
|
return total_region.size
|
||||||
|
|
||||||
virtual_size = add_widget(root, size.region, (), size.region)
|
virtual_size = add_widget(root, size.region, (), size.region)
|
||||||
self.widgets.clear()
|
return map, virtual_size, widgets
|
||||||
self.widgets.update(widgets)
|
|
||||||
return map, virtual_size
|
|
||||||
|
|
||||||
async def mount_all(self, view: "View") -> None:
|
async def mount_all(self, screen: Screen) -> None:
|
||||||
view.mount(*self.widgets)
|
screen.app.mount(*self.widgets)
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[tuple[Widget, Region, Region]]:
|
def __iter__(self) -> Iterator[tuple[Widget, Region, Region]]:
|
||||||
layers = sorted(self.map.items(), key=lambda item: item[1].order, reverse=True)
|
layers = sorted(self.map.items(), key=lambda item: item[1].order, reverse=True)
|
||||||
@@ -244,14 +238,14 @@ class Compositor:
|
|||||||
try:
|
try:
|
||||||
return self.map[widget].region.origin
|
return self.map[widget].region.origin
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise NoWidget("Widget is not in layout")
|
raise errors.NoWidget("Widget is not in layout")
|
||||||
|
|
||||||
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
|
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
|
||||||
"""Get the widget under the given point or None."""
|
"""Get the widget under the given point or None."""
|
||||||
for widget, cropped_region, region in self:
|
for widget, cropped_region, region in self:
|
||||||
if widget.is_visual and cropped_region.contains(x, y):
|
if cropped_region.contains(x, y):
|
||||||
return widget, region
|
return widget, region
|
||||||
raise NoWidget(f"No widget under screen coordinate ({x}, {y})")
|
raise errors.NoWidget(f"No widget under screen coordinate ({x}, {y})")
|
||||||
|
|
||||||
def get_style_at(self, x: int, y: int) -> Style:
|
def get_style_at(self, x: int, y: int) -> Style:
|
||||||
"""Get the Style at the given cell or Style.null()
|
"""Get the Style at the given cell or Style.null()
|
||||||
@@ -265,13 +259,15 @@ class Compositor:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
widget, region = self.get_widget_at(x, y)
|
widget, region = self.get_widget_at(x, y)
|
||||||
except NoWidget:
|
except errors.NoWidget:
|
||||||
return Style.null()
|
return Style.null()
|
||||||
if widget not in self.regions:
|
if widget not in self.regions:
|
||||||
return Style.null()
|
return Style.null()
|
||||||
lines = widget._get_lines()
|
lines = widget._get_lines()
|
||||||
x -= region.x
|
x -= region.x
|
||||||
y -= region.y
|
y -= region.y
|
||||||
|
if y > len(lines):
|
||||||
|
return Style.null()
|
||||||
line = lines[y]
|
line = lines[y]
|
||||||
end = 0
|
end = 0
|
||||||
for segment in line:
|
for segment in line:
|
||||||
@@ -296,7 +292,7 @@ class Compositor:
|
|||||||
try:
|
try:
|
||||||
region, *_ = self.map[widget]
|
region, *_ = self.map[widget]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise NoWidget("Widget is not in layout")
|
raise errors.NoWidget("Widget is not in layout")
|
||||||
else:
|
else:
|
||||||
return region
|
return region
|
||||||
|
|
||||||
@@ -344,7 +340,7 @@ class Compositor:
|
|||||||
|
|
||||||
for widget, region, _order, clip in widget_regions:
|
for widget, region, _order, clip in widget_regions:
|
||||||
|
|
||||||
if not (widget.is_visual and widget.visible):
|
if not (widget.visible and widget.is_visual):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
lines = widget._get_lines()
|
lines = widget._get_lines()
|
||||||
@@ -465,5 +461,4 @@ class Compositor:
|
|||||||
update_region = region.intersection(clip)
|
update_region = region.intersection(clip)
|
||||||
update_lines = self.render(console, crop=update_region).lines
|
update_lines = self.render(console, crop=update_region).lines
|
||||||
update = LayoutUpdate(update_lines, update_region)
|
update = LayoutUpdate(update_lines, update_region)
|
||||||
log(update)
|
|
||||||
return update
|
return update
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ class NodeList:
|
|||||||
self._node_refs: list[ref[DOMNode]] = []
|
self._node_refs: list[ref[DOMNode]] = []
|
||||||
self.__nodes: list[DOMNode] | None = []
|
self.__nodes: list[DOMNode] | None = []
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
self._prune()
|
||||||
|
return bool(self._node_refs)
|
||||||
|
|
||||||
|
def __length_hint__(self) -> int:
|
||||||
|
return len(self._node_refs)
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield self._widgets
|
yield self._widgets
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class Timer:
|
|||||||
*,
|
*,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
callback: TimerCallback | None = None,
|
callback: TimerCallback | None = None,
|
||||||
repeat: int = None,
|
repeat: int | None = None,
|
||||||
skip: bool = False,
|
skip: bool = False,
|
||||||
pause: bool = False,
|
pause: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ else:
|
|||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .events import Event
|
|
||||||
from .message import Message
|
from .message import Message
|
||||||
|
|
||||||
Callback = Callable[[], None]
|
Callback = Callable[[], None]
|
||||||
# IntervalID = int
|
|
||||||
|
|
||||||
|
|
||||||
class MessageTarget(Protocol):
|
class MessageTarget(Protocol):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import rich.repr
|
|||||||
from rich.console import Console, RenderableType
|
from rich.console import Console, RenderableType
|
||||||
from rich.control import Control
|
from rich.control import Control
|
||||||
from rich.measure import Measurement
|
from rich.measure import Measurement
|
||||||
from rich.screen import Screen
|
from rich.screen import Screen as ScreenRenderable
|
||||||
from rich.traceback import Traceback
|
from rich.traceback import Traceback
|
||||||
|
|
||||||
from . import actions
|
from . import actions
|
||||||
@@ -34,10 +34,9 @@ from .layouts.dock import Dock
|
|||||||
from .message_pump import MessagePump
|
from .message_pump import MessagePump
|
||||||
from .reactive import Reactive
|
from .reactive import Reactive
|
||||||
from .renderables.gradient import VerticalGradient
|
from .renderables.gradient import VerticalGradient
|
||||||
from .view import View
|
from .screen import Screen
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
|
|
||||||
from .css.query import NoMatchingNodesError
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .css.query import DOMQuery
|
from .css.query import DOMQuery
|
||||||
@@ -50,8 +49,6 @@ warnings.simplefilter("always", ResourceWarning)
|
|||||||
|
|
||||||
LayoutDefinition = "dict[str, Any]"
|
LayoutDefinition = "dict[str, Any]"
|
||||||
|
|
||||||
ViewType = TypeVar("ViewType", bound=View)
|
|
||||||
|
|
||||||
|
|
||||||
class AppError(Exception):
|
class AppError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -86,12 +83,12 @@ class App(DOMNode):
|
|||||||
driver_class (Type[Driver], optional): Driver class, or None to use default. Defaults to None.
|
driver_class (Type[Driver], optional): Driver class, or None to use default. Defaults to None.
|
||||||
title (str, optional): Title of the application. Defaults to "Textual Application".
|
title (str, optional): Title of the application. Defaults to "Textual Application".
|
||||||
"""
|
"""
|
||||||
self.console = Console()
|
self.console = Console(markup=False)
|
||||||
self.error_console = Console(stderr=True)
|
self.error_console = Console(markup=False, stderr=True)
|
||||||
self._screen = screen
|
self._screen = screen
|
||||||
self.driver_class = driver_class or self.get_driver_class()
|
self.driver_class = driver_class or self.get_driver_class()
|
||||||
self._title = title
|
self._title = title
|
||||||
self._view_stack: list[View] = []
|
self._screen_stack: list[Screen] = []
|
||||||
|
|
||||||
self.focused: Widget | None = None
|
self.focused: Widget | None = None
|
||||||
self.mouse_over: Widget | None = None
|
self.mouse_over: Widget | None = None
|
||||||
@@ -100,7 +97,7 @@ class App(DOMNode):
|
|||||||
self._exit_renderables: list[RenderableType] = []
|
self._exit_renderables: list[RenderableType] = []
|
||||||
|
|
||||||
self._docks: list[Dock] = []
|
self._docks: list[Dock] = []
|
||||||
self._action_targets = {"app", "view"}
|
self._action_targets = {"app", "screen"}
|
||||||
self._animator = Animator(self)
|
self._animator = Animator(self)
|
||||||
self.animate = self._animator.bind(self)
|
self.animate = self._animator.bind(self)
|
||||||
self.mouse_position = Offset(0, 0)
|
self.mouse_position = Offset(0, 0)
|
||||||
@@ -158,8 +155,8 @@ class App(DOMNode):
|
|||||||
return self._animator
|
return self._animator
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def view(self) -> View:
|
def screen(self) -> Screen:
|
||||||
return self._view_stack[-1]
|
return self._screen_stack[-1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def css_type(self) -> str:
|
def css_type(self) -> str:
|
||||||
@@ -262,10 +259,10 @@ class App(DOMNode):
|
|||||||
self.reset_styles()
|
self.reset_styles()
|
||||||
self.stylesheet = stylesheet
|
self.stylesheet = stylesheet
|
||||||
self.stylesheet.update(self)
|
self.stylesheet.update(self)
|
||||||
self.view.refresh(layout=True)
|
self.screen.refresh(layout=True)
|
||||||
|
|
||||||
def query(self, selector: str | None = None) -> DOMQuery:
|
def query(self, selector: str | None = None) -> DOMQuery:
|
||||||
"""Get a DOM query in the current view.
|
"""Get a DOM query in the current screen.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
selector (str, optional): A CSS selector or `None` for all nodes. Defaults to None.
|
selector (str, optional): A CSS selector or `None` for all nodes. Defaults to None.
|
||||||
@@ -275,10 +272,10 @@ class App(DOMNode):
|
|||||||
"""
|
"""
|
||||||
from .css.query import DOMQuery
|
from .css.query import DOMQuery
|
||||||
|
|
||||||
return DOMQuery(self.view, selector)
|
return DOMQuery(self.screen, selector)
|
||||||
|
|
||||||
def get_child(self, id: str) -> DOMNode:
|
def get_child(self, id: str) -> DOMNode:
|
||||||
"""Shorthand for self.view.get_child(id: str)
|
"""Shorthand for self.screen.get_child(id: str)
|
||||||
Returns the first child (immediate descendent) of this DOMNode
|
Returns the first child (immediate descendent) of this DOMNode
|
||||||
with the given ID.
|
with the given ID.
|
||||||
|
|
||||||
@@ -288,7 +285,7 @@ class App(DOMNode):
|
|||||||
Returns:
|
Returns:
|
||||||
DOMNode: The first child of this node with the specified ID.
|
DOMNode: The first child of this node with the specified ID.
|
||||||
"""
|
"""
|
||||||
return self.view.get_child(id)
|
return self.screen.get_child(id)
|
||||||
|
|
||||||
def render_background(self) -> RenderableType:
|
def render_background(self) -> RenderableType:
|
||||||
gradient = VerticalGradient("red", "blue")
|
gradient = VerticalGradient("red", "blue")
|
||||||
@@ -303,12 +300,12 @@ class App(DOMNode):
|
|||||||
self.post_message_no_wait(messages.StylesUpdated(self))
|
self.post_message_no_wait(messages.StylesUpdated(self))
|
||||||
|
|
||||||
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
|
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
|
||||||
self.register(self.view, *anon_widgets, **widgets)
|
self.register(self.screen, *anon_widgets, **widgets)
|
||||||
self.view.refresh()
|
self.screen.refresh()
|
||||||
|
|
||||||
async def push_view(self, view: ViewType) -> ViewType:
|
async def push_screen(self, screen: Screen) -> Screen:
|
||||||
self._view_stack.append(view)
|
self._screen_stack.append(screen)
|
||||||
return view
|
return screen
|
||||||
|
|
||||||
async def set_focus(self, widget: Widget | None) -> None:
|
async def set_focus(self, widget: Widget | None) -> None:
|
||||||
"""Focus (or unfocus) a widget. A focused widget will receive key events first.
|
"""Focus (or unfocus) a widget. A focused widget will receive key events first.
|
||||||
@@ -450,7 +447,7 @@ class App(DOMNode):
|
|||||||
|
|
||||||
def _register_child(self, parent: DOMNode, child: DOMNode) -> bool:
|
def _register_child(self, parent: DOMNode, child: DOMNode) -> bool:
|
||||||
if child not in self.registry:
|
if child not in self.registry:
|
||||||
parent.children._append(child)
|
parent.node_list._append(child)
|
||||||
self.registry.add(child)
|
self.registry.add(child)
|
||||||
child.set_parent(parent)
|
child.set_parent(parent)
|
||||||
child.start_messages()
|
child.start_messages()
|
||||||
@@ -473,6 +470,11 @@ class App(DOMNode):
|
|||||||
name_widgets = [*((None, widget) for widget in anon_widgets), *widgets.items()]
|
name_widgets = [*((None, widget) for widget in anon_widgets), *widgets.items()]
|
||||||
apply_stylesheet = self.stylesheet.apply
|
apply_stylesheet = self.stylesheet.apply
|
||||||
|
|
||||||
|
# Register children
|
||||||
|
for _widget_id, widget in name_widgets:
|
||||||
|
if widget.node_list:
|
||||||
|
self.register(widget, *widget.children)
|
||||||
|
|
||||||
for widget_id, widget in name_widgets:
|
for widget_id, widget in name_widgets:
|
||||||
if widget not in self.registry:
|
if widget not in self.registry:
|
||||||
if widget_id is not None:
|
if widget_id is not None:
|
||||||
@@ -500,7 +502,9 @@ class App(DOMNode):
|
|||||||
driver.disable_input()
|
driver.disable_input()
|
||||||
await self.close_messages()
|
await self.close_messages()
|
||||||
|
|
||||||
def refresh(self) -> None:
|
def refresh(self, *, repaint: bool = True, layout: bool = False) -> None:
|
||||||
|
if not self._running:
|
||||||
|
return
|
||||||
sync_available = (
|
sync_available = (
|
||||||
os.environ.get("TERM_PROGRAM", "") != "Apple_Terminal" and not WINDOWS
|
os.environ.get("TERM_PROGRAM", "") != "Apple_Terminal" and not WINDOWS
|
||||||
)
|
)
|
||||||
@@ -509,9 +513,7 @@ class App(DOMNode):
|
|||||||
try:
|
try:
|
||||||
if sync_available:
|
if sync_available:
|
||||||
console.file.write("\x1bP=1s\x1b\\")
|
console.file.write("\x1bP=1s\x1b\\")
|
||||||
console.print(
|
console.print(ScreenRenderable(Control.home(), self.screen.render()))
|
||||||
Screen(Control.home(), self.view.render_styled(), Control.home())
|
|
||||||
)
|
|
||||||
if sync_available:
|
if sync_available:
|
||||||
console.file.write("\x1bP=2s\x1b\\")
|
console.file.write("\x1bP=2s\x1b\\")
|
||||||
console.file.flush()
|
console.file.flush()
|
||||||
@@ -519,6 +521,8 @@ class App(DOMNode):
|
|||||||
self.panic()
|
self.panic()
|
||||||
|
|
||||||
def display(self, renderable: RenderableType) -> None:
|
def display(self, renderable: RenderableType) -> None:
|
||||||
|
if not self._running:
|
||||||
|
return
|
||||||
if not self._closed:
|
if not self._closed:
|
||||||
console = self.console
|
console = self.console
|
||||||
try:
|
try:
|
||||||
@@ -551,7 +555,7 @@ class App(DOMNode):
|
|||||||
Returns:
|
Returns:
|
||||||
tuple[Widget, Region]: The widget and the widget's screen region.
|
tuple[Widget, Region]: The widget and the widget's screen region.
|
||||||
"""
|
"""
|
||||||
return self.view.get_widget_at(x, y)
|
return self.screen.get_widget_at(x, y)
|
||||||
|
|
||||||
def bell(self) -> None:
|
def bell(self) -> None:
|
||||||
"""Play the console 'bell'."""
|
"""Play the console 'bell'."""
|
||||||
@@ -578,9 +582,9 @@ class App(DOMNode):
|
|||||||
# Handle input events that haven't been forwarded
|
# Handle input events that haven't been forwarded
|
||||||
# If the event has been forwarded it may have bubbled up back to the App
|
# If the event has been forwarded it may have bubbled up back to the App
|
||||||
if isinstance(event, events.Mount):
|
if isinstance(event, events.Mount):
|
||||||
view = View()
|
screen = Screen()
|
||||||
self.register(self, view)
|
self.register(self, screen)
|
||||||
await self.push_view(view)
|
await self.push_screen(screen)
|
||||||
await super().on_event(event)
|
await super().on_event(event)
|
||||||
|
|
||||||
elif isinstance(event, events.InputEvent) and not event.is_forwarded:
|
elif isinstance(event, events.InputEvent) and not event.is_forwarded:
|
||||||
@@ -596,7 +600,7 @@ class App(DOMNode):
|
|||||||
await super().on_event(event)
|
await super().on_event(event)
|
||||||
else:
|
else:
|
||||||
# Forward the event to the view
|
# Forward the event to the view
|
||||||
await self.view.forward_event(event)
|
await self.screen.forward_event(event)
|
||||||
else:
|
else:
|
||||||
await super().on_event(event)
|
await super().on_event(event)
|
||||||
|
|
||||||
@@ -636,6 +640,16 @@ class App(DOMNode):
|
|||||||
async def broker_event(
|
async def broker_event(
|
||||||
self, event_name: str, event: events.Event, default_namespace: object | None
|
self, event_name: str, event: events.Event, default_namespace: object | None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
"""Allow the app an opportunity to dispatch events to action system.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_name (str): _description_
|
||||||
|
event (events.Event): An event object.
|
||||||
|
default_namespace (object | None): _description_
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: _description_
|
||||||
|
"""
|
||||||
event.stop()
|
event.stop()
|
||||||
try:
|
try:
|
||||||
style = getattr(event, "style")
|
style = getattr(event, "style")
|
||||||
@@ -661,7 +675,7 @@ class App(DOMNode):
|
|||||||
|
|
||||||
async def handle_layout(self, message: messages.Layout) -> None:
|
async def handle_layout(self, message: messages.Layout) -> None:
|
||||||
message.stop()
|
message.stop()
|
||||||
# await self.view.refresh_layout()
|
# await self.screen.refresh_layout()
|
||||||
self.app.refresh()
|
self.app.refresh()
|
||||||
|
|
||||||
async def on_key(self, event: events.Key) -> None:
|
async def on_key(self, event: events.Key) -> None:
|
||||||
@@ -672,7 +686,7 @@ class App(DOMNode):
|
|||||||
await self.close_messages()
|
await self.close_messages()
|
||||||
|
|
||||||
async def on_resize(self, event: events.Resize) -> None:
|
async def on_resize(self, event: events.Resize) -> None:
|
||||||
await self.view.post_message(event)
|
await self.screen.post_message(event)
|
||||||
|
|
||||||
async def action_press(self, key: str) -> None:
|
async def action_press(self, key: str) -> None:
|
||||||
await self.press(key)
|
await self.press(key)
|
||||||
@@ -687,13 +701,13 @@ class App(DOMNode):
|
|||||||
self.bell()
|
self.bell()
|
||||||
|
|
||||||
async def action_add_class_(self, selector: str, class_name: str) -> None:
|
async def action_add_class_(self, selector: str, class_name: str) -> None:
|
||||||
self.view.query(selector).add_class(class_name)
|
self.screen.query(selector).add_class(class_name)
|
||||||
|
|
||||||
async def action_remove_class_(self, selector: str, class_name: str) -> None:
|
async def action_remove_class_(self, selector: str, class_name: str) -> None:
|
||||||
self.view.query(selector).remove_class(class_name)
|
self.screen.query(selector).remove_class(class_name)
|
||||||
|
|
||||||
async def action_toggle_class(self, selector: str, class_name: str) -> None:
|
async def action_toggle_class(self, selector: str, class_name: str) -> None:
|
||||||
self.view.query(selector).toggle_class(class_name)
|
self.screen.query(selector).toggle_class(class_name)
|
||||||
|
|
||||||
async def handle_styles_updated(self, message: messages.StylesUpdated) -> None:
|
async def handle_styles_updated(self, message: messages.StylesUpdated) -> None:
|
||||||
self.stylesheet.update(self)
|
self.stylesheet.update(self)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import rich.repr
|
|||||||
from rich.color import Color
|
from rich.color import Color
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
|
from .. import log
|
||||||
from ._error_tools import friendly_list
|
from ._error_tools import friendly_list
|
||||||
from .constants import NULL_SPACING
|
from .constants import NULL_SPACING
|
||||||
from .errors import StyleTypeError, StyleValueError
|
from .errors import StyleTypeError, StyleValueError
|
||||||
@@ -537,7 +538,6 @@ class OffsetProperty:
|
|||||||
ScalarParseError: If any of the string values supplied in the 2-tuple cannot
|
ScalarParseError: If any of the string values supplied in the 2-tuple cannot
|
||||||
be parsed into a Scalar. For example, if you specify an non-existent unit.
|
be parsed into a Scalar. For example, if you specify an non-existent unit.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if offset is None:
|
if offset is None:
|
||||||
if obj.clear_rule(self.name):
|
if obj.clear_rule(self.name):
|
||||||
obj.refresh(layout=True)
|
obj.refresh(layout=True)
|
||||||
@@ -557,6 +557,7 @@ class OffsetProperty:
|
|||||||
else Scalar(float(y), Unit.CELLS, Unit.HEIGHT)
|
else Scalar(float(y), Unit.CELLS, Unit.HEIGHT)
|
||||||
)
|
)
|
||||||
_offset = ScalarOffset(scalar_x, scalar_y)
|
_offset = ScalarOffset(scalar_x, scalar_y)
|
||||||
|
|
||||||
if obj.set_rule(self.name, _offset):
|
if obj.set_rule(self.name, _offset):
|
||||||
obj.refresh(layout=True)
|
obj.refresh(layout=True)
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class DOMQuery:
|
|||||||
node.set_styles(css, **styles)
|
node.set_styles(css, **styles)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def refresh(self, repaint: bool = True, layout: bool = False) -> DOMQuery:
|
def refresh(self, *, repaint: bool = True, layout: bool = False) -> DOMQuery:
|
||||||
"""Refresh matched nodes.
|
"""Refresh matched nodes.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from .. import events
|
from .. import events, log
|
||||||
from ..geometry import Offset
|
from ..geometry import Offset
|
||||||
from .._animator import Animation
|
from .._animator import Animation
|
||||||
from .scalar import ScalarOffset
|
from .scalar import ScalarOffset
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import rich.repr
|
|||||||
from rich.color import Color
|
from rich.color import Color
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
|
from .. import log
|
||||||
from .._animator import Animation, EasingFunction
|
from .._animator import Animation, EasingFunction
|
||||||
from ..geometry import Spacing
|
from ..geometry import Spacing
|
||||||
from ._style_properties import (
|
from ._style_properties import (
|
||||||
@@ -361,6 +362,7 @@ class Styles(StylesBase):
|
|||||||
return self._rules.get(rule, default)
|
return self._rules.get(rule, default)
|
||||||
|
|
||||||
def refresh(self, *, layout: bool = False) -> None:
|
def refresh(self, *, layout: bool = False) -> None:
|
||||||
|
return
|
||||||
self._repaint_required = True
|
self._repaint_required = True
|
||||||
self._layout_required = self._layout_required or layout
|
self._layout_required = self._layout_required or layout
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import rich.repr
|
|||||||
from rich.highlighter import ReprHighlighter
|
from rich.highlighter import ReprHighlighter
|
||||||
from rich.pretty import Pretty
|
from rich.pretty import Pretty
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
from rich.text import Text
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from ._node_list import NodeList
|
from ._node_list import NodeList
|
||||||
@@ -19,7 +20,7 @@ from .message_pump import MessagePump
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .css.query import DOMQuery
|
from .css.query import DOMQuery
|
||||||
from .view import View
|
from .screen import Screen
|
||||||
|
|
||||||
|
|
||||||
class NoParent(Exception):
|
class NoParent(Exception):
|
||||||
@@ -37,11 +38,16 @@ class DOMNode(MessagePump):
|
|||||||
DEFAULT_STYLES = ""
|
DEFAULT_STYLES = ""
|
||||||
INLINE_STYLES = ""
|
INLINE_STYLES = ""
|
||||||
|
|
||||||
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str | None = None,
|
||||||
|
id: str | None = None,
|
||||||
|
classes: Iterable[str] | None = None,
|
||||||
|
) -> None:
|
||||||
self._name = name
|
self._name = name
|
||||||
self._id = id
|
self._id = id
|
||||||
self._classes: set[str] = set()
|
self._classes: set[str] = set(classes) if classes else set()
|
||||||
self.children = NodeList()
|
self.node_list = NodeList()
|
||||||
self._css_styles: Styles = Styles(self)
|
self._css_styles: Styles = Styles(self)
|
||||||
self._inline_styles: Styles = Styles.parse(
|
self._inline_styles: Styles = Styles.parse(
|
||||||
self.INLINE_STYLES, repr(self), node=self
|
self.INLINE_STYLES, repr(self), node=self
|
||||||
@@ -73,18 +79,23 @@ class DOMNode(MessagePump):
|
|||||||
return self._parent
|
return self._parent
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def view(self) -> "View":
|
def screen(self) -> "Screen":
|
||||||
"""Get the current view."""
|
"""Get the current screen."""
|
||||||
# Get the node by looking up a chain of parents
|
# Get the node by looking up a chain of parents
|
||||||
# Note that self.view may not be the same as self.app.view
|
# Note that self.screen may not be the same as self.app.screen
|
||||||
from .view import View
|
from .screen import Screen
|
||||||
|
|
||||||
node = self
|
node = self
|
||||||
while node and not isinstance(node, View):
|
while node and not isinstance(node, Screen):
|
||||||
node = node._parent
|
node = node._parent
|
||||||
assert isinstance(node, View)
|
assert isinstance(node, Screen)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_visual(self) -> bool:
|
||||||
|
"""Check if the widget is visual (i.e. draws something on Screen)."""
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self) -> str | None:
|
def id(self) -> str | None:
|
||||||
"""The ID of this node, or None if the node has no ID.
|
"""The ID of this node, or None if the node has no ID.
|
||||||
@@ -116,6 +127,22 @@ class DOMNode(MessagePump):
|
|||||||
def name(self) -> str | None:
|
def name(self) -> str | None:
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def css_identifier(self) -> str:
|
||||||
|
tokens = [self.__class__.__name__]
|
||||||
|
if self.id is not None:
|
||||||
|
tokens.append(f"#{self.id}")
|
||||||
|
return "".join(tokens)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def css_identifier_styled(self) -> Text:
|
||||||
|
tokens = Text(self.__class__.__name__)
|
||||||
|
if self.id is not None:
|
||||||
|
tokens.append(f"#{self.id}", style="bold")
|
||||||
|
if self.name:
|
||||||
|
tokens.append(f"[name={self.name}]", style="underline")
|
||||||
|
return tokens
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def classes(self) -> frozenset[str]:
|
def classes(self) -> frozenset[str]:
|
||||||
return frozenset(self._classes)
|
return frozenset(self._classes)
|
||||||
@@ -237,12 +264,25 @@ class DOMNode(MessagePump):
|
|||||||
Returns:
|
Returns:
|
||||||
Tree: A Rich object which may be printed.
|
Tree: A Rich object which may be printed.
|
||||||
"""
|
"""
|
||||||
|
from rich.columns import Columns
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
highlighter = ReprHighlighter()
|
highlighter = ReprHighlighter()
|
||||||
tree = Tree(highlighter(repr(self)))
|
tree = Tree(highlighter(repr(self)))
|
||||||
|
|
||||||
def add_children(tree, node):
|
def add_children(tree, node):
|
||||||
for child in node.children:
|
for child in node.node_list:
|
||||||
branch = tree.add(Pretty(child))
|
branch = tree.add(
|
||||||
|
Columns(
|
||||||
|
[
|
||||||
|
Pretty(child),
|
||||||
|
Text(
|
||||||
|
f"{child.size.width} X {child.size.height}", style="dim"
|
||||||
|
),
|
||||||
|
Panel(Text(child.styles.css), border_style="dim"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
if tree.children:
|
if tree.children:
|
||||||
add_children(branch, child)
|
add_children(branch, child)
|
||||||
|
|
||||||
@@ -276,12 +316,20 @@ class DOMNode(MessagePump):
|
|||||||
Args:
|
Args:
|
||||||
node (DOMNode): A DOM node.
|
node (DOMNode): A DOM node.
|
||||||
"""
|
"""
|
||||||
self.children._append(node)
|
self.node_list._append(node)
|
||||||
node.set_parent(self)
|
node.set_parent(self)
|
||||||
|
|
||||||
|
def add_children(self, *nodes: DOMNode, **named_nodes: DOMNode) -> None:
|
||||||
|
_append = self.node_list._append
|
||||||
|
for node in nodes:
|
||||||
|
_append(node)
|
||||||
|
for node_id, node in named_nodes.items():
|
||||||
|
_append(node)
|
||||||
|
node.id = node_id
|
||||||
|
|
||||||
def walk_children(self, with_self: bool = True) -> Iterable[DOMNode]:
|
def walk_children(self, with_self: bool = True) -> Iterable[DOMNode]:
|
||||||
|
|
||||||
stack: list[Iterator[DOMNode]] = [iter(self.children)]
|
stack: list[Iterator[DOMNode]] = [iter(self.node_list)]
|
||||||
pop = stack.pop
|
pop = stack.pop
|
||||||
push = stack.append
|
push = stack.append
|
||||||
|
|
||||||
@@ -294,8 +342,8 @@ class DOMNode(MessagePump):
|
|||||||
pop()
|
pop()
|
||||||
else:
|
else:
|
||||||
yield node
|
yield node
|
||||||
if node.children:
|
if node.node_list:
|
||||||
push(iter(node.children))
|
push(iter(node.node_list))
|
||||||
|
|
||||||
def get_child(self, id: str) -> DOMNode:
|
def get_child(self, id: str) -> DOMNode:
|
||||||
"""Return the first child (immediate descendent) of this node with the given ID.
|
"""Return the first child (immediate descendent) of this node with the given ID.
|
||||||
@@ -306,7 +354,7 @@ class DOMNode(MessagePump):
|
|||||||
Returns:
|
Returns:
|
||||||
DOMNode: The first child of this node with the ID.
|
DOMNode: The first child of this node with the ID.
|
||||||
"""
|
"""
|
||||||
for child in self.children:
|
for child in self.node_list:
|
||||||
if child.id == id:
|
if child.id == id:
|
||||||
return child
|
return child
|
||||||
raise NoMatchingNodesError(f"No child found with id={id!r}")
|
raise NoMatchingNodesError(f"No child found with id={id!r}")
|
||||||
@@ -379,5 +427,5 @@ class DOMNode(MessagePump):
|
|||||||
has_pseudo_classes = self.pseudo_classes.issuperset(class_names)
|
has_pseudo_classes = self.pseudo_classes.issuperset(class_names)
|
||||||
return has_pseudo_classes
|
return has_pseudo_classes
|
||||||
|
|
||||||
def refresh(self, repaint: bool = True, layout: bool = False) -> None:
|
def refresh(self, *, repaint: bool = True, layout: bool = False) -> None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
class MissingWidget(Exception):
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class TextualError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoWidget(TextualError):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from typing import Awaitable, Callable, Type, TYPE_CHECKING, TypeVar
|
|||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
|
from . import log
|
||||||
from .geometry import Offset, Size
|
from .geometry import Offset, Size
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from ._types import MessageTarget
|
from ._types import MessageTarget
|
||||||
@@ -389,8 +390,3 @@ class Focus(Event, bubble=False):
|
|||||||
|
|
||||||
class Blur(Event, bubble=False):
|
class Blur(Event, bubble=False):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# class Update(Event, bubble=False):
|
|
||||||
# def can_replace(self, event: Message) -> bool:
|
|
||||||
# return isinstance(event, Update) and event.sender == self.sender
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import ClassVar, Generator, Iterable, NamedTuple, Sequence, TYPE_CHECKING
|
from typing import ClassVar, Iterable, NamedTuple, TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
from .geometry import Region, Offset, Size
|
from .geometry import Region, Offset, Size
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .dom import DOMNode
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
from .view import View
|
from .screen import Screen
|
||||||
|
|
||||||
|
|
||||||
class WidgetPlacement(NamedTuple):
|
class WidgetPlacement(NamedTuple):
|
||||||
@@ -47,8 +48,8 @@ class Layout(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def arrange(
|
def arrange(
|
||||||
self, parent: View, size: Size, scroll: Offset
|
self, parent: Screen, size: Size, scroll: Offset
|
||||||
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
) -> tuple[Iterable[WidgetPlacement], set[Widget]]:
|
||||||
"""Generate a layout map that defines where on the screen the widgets will be drawn.
|
"""Generate a layout map that defines where on the screen the widgets will be drawn.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
"""
|
|
||||||
|
|
||||||
Planned for deprecation
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
|
||||||
from typing import ItemsView, KeysView, ValuesView, NamedTuple
|
|
||||||
|
|
||||||
from . import log
|
|
||||||
from .geometry import Offset, Region, Size
|
|
||||||
from operator import attrgetter
|
|
||||||
from .widget import Widget
|
|
||||||
|
|
||||||
|
|
||||||
class RenderRegion(NamedTuple):
|
|
||||||
"""Defines the absolute location of a Widget."""
|
|
||||||
|
|
||||||
region: Region
|
|
||||||
order: tuple[int, ...]
|
|
||||||
clip: Region
|
|
||||||
|
|
||||||
|
|
||||||
class LayoutMap:
|
|
||||||
"""A container that maps widgets on to their absolute location."""
|
|
||||||
|
|
||||||
def __init__(self, size: Size) -> None:
|
|
||||||
self.size = size
|
|
||||||
self.widgets: dict[Widget, RenderRegion] = {}
|
|
||||||
|
|
||||||
def __getitem__(self, widget: Widget) -> RenderRegion:
|
|
||||||
return self.widgets[widget]
|
|
||||||
|
|
||||||
def items(self) -> ItemsView[Widget, RenderRegion]:
|
|
||||||
return self.widgets.items()
|
|
||||||
|
|
||||||
def keys(self) -> KeysView[Widget]:
|
|
||||||
return self.widgets.keys()
|
|
||||||
|
|
||||||
def values(self) -> ValuesView[RenderRegion]:
|
|
||||||
return self.widgets.values()
|
|
||||||
|
|
||||||
def clear(self) -> None:
|
|
||||||
self.widgets.clear()
|
|
||||||
|
|
||||||
def add_widget(
|
|
||||||
self,
|
|
||||||
widget: Widget,
|
|
||||||
region: Region,
|
|
||||||
order: tuple[int, ...],
|
|
||||||
clip: Region,
|
|
||||||
) -> None:
|
|
||||||
from .view import View
|
|
||||||
|
|
||||||
if widget in self.widgets:
|
|
||||||
return
|
|
||||||
|
|
||||||
layout_offset = Offset(0, 0)
|
|
||||||
if any(widget.styles.offset):
|
|
||||||
layout_offset = widget.styles.offset.resolve(region.size, clip.size)
|
|
||||||
|
|
||||||
self.widgets[widget] = RenderRegion(region + layout_offset, order, clip)
|
|
||||||
|
|
||||||
# TODO: replace with widget.layout
|
|
||||||
|
|
||||||
if isinstance(widget, View):
|
|
||||||
view: View = widget
|
|
||||||
scroll = view.scroll
|
|
||||||
total_region = region.size.region
|
|
||||||
sub_clip = clip.intersection(region)
|
|
||||||
|
|
||||||
arrangement = sorted(
|
|
||||||
view.get_arrangement(region.size, scroll), key=attrgetter("order")
|
|
||||||
)
|
|
||||||
for sub_region, sub_widget, z in arrangement:
|
|
||||||
total_region = total_region.union(sub_region)
|
|
||||||
if sub_widget is not None:
|
|
||||||
self.add_widget(
|
|
||||||
sub_widget,
|
|
||||||
sub_region + region.origin - scroll,
|
|
||||||
sub_widget.z + (z,),
|
|
||||||
sub_clip,
|
|
||||||
)
|
|
||||||
view.virtual_size = total_region.size
|
|
||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, NamedTuple, Sequence
|
from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
|
||||||
|
|
||||||
from .._layout_resolve import layout_resolve
|
from .._layout_resolve import layout_resolve
|
||||||
from ..css.types import Edge
|
from ..css.types import Edge
|
||||||
@@ -17,7 +17,7 @@ else:
|
|||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..view import View
|
from ..screen import Screen
|
||||||
|
|
||||||
DockEdge = Literal["top", "right", "bottom", "left"]
|
DockEdge = Literal["top", "right", "bottom", "left"]
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ class DockLayout(Layout):
|
|||||||
|
|
||||||
def arrange(
|
def arrange(
|
||||||
self, parent: Widget, size: Size, scroll: Offset
|
self, parent: Widget, size: Size, scroll: Offset
|
||||||
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
) -> tuple[Iterable[WidgetPlacement], set[Widget]]:
|
||||||
|
|
||||||
width, height = size
|
width, height = size
|
||||||
layout_region = Region(0, 0, width, height)
|
layout_region = Region(0, 0, width, height)
|
||||||
@@ -69,7 +69,7 @@ class DockLayout(Layout):
|
|||||||
|
|
||||||
docks = self.get_docks(parent)
|
docks = self.get_docks(parent)
|
||||||
|
|
||||||
def make_dock_options(widget, edge: Edge) -> DockOptions:
|
def make_dock_options(widget: Widget, edge: Edge) -> DockOptions:
|
||||||
styles = widget.styles
|
styles = widget.styles
|
||||||
has_rule = styles.has_rule
|
has_rule = styles.has_rule
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from ..layout import Layout, WidgetPlacement
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
from ..view import View
|
from ..screen import Screen
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
@@ -266,7 +266,7 @@ class GridLayout(Layout):
|
|||||||
return self.widgets.keys()
|
return self.widgets.keys()
|
||||||
|
|
||||||
def arrange(
|
def arrange(
|
||||||
self, view: View, size: Size, scroll: Offset
|
self, view: Screen, size: Size, scroll: Offset
|
||||||
) -> Iterable[WidgetPlacement]:
|
) -> Iterable[WidgetPlacement]:
|
||||||
"""Generate a map that associates widgets with their location on screen.
|
"""Generate a map that associates widgets with their location on screen.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from textual._loop import loop_last
|
|||||||
from textual.css.styles import Styles
|
from textual.css.styles import Styles
|
||||||
from textual.geometry import Size, Offset, Region
|
from textual.geometry import Size, Offset, Region
|
||||||
from textual.layout import Layout, WidgetPlacement
|
from textual.layout import Layout, WidgetPlacement
|
||||||
from textual.view import View
|
from textual.screen import Screen
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
|
|
||||||
|
|
||||||
@@ -15,26 +15,29 @@ class HorizontalLayout(Layout):
|
|||||||
fill the space of their parent container, all widgets used in a horizontal layout should have a specified.
|
fill the space of their parent container, all widgets used in a horizontal layout should have a specified.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_widgets(self, view: View) -> Iterable[Widget]:
|
|
||||||
return view.children
|
|
||||||
|
|
||||||
def arrange(
|
def arrange(
|
||||||
self, view: View, size: Size, scroll: Offset
|
self, parent: Widget, size: Size, scroll: Offset
|
||||||
) -> Iterable[WidgetPlacement]:
|
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
||||||
|
|
||||||
|
placements: list[WidgetPlacement] = []
|
||||||
|
add_placement = placements.append
|
||||||
|
|
||||||
parent_width, parent_height = size
|
parent_width, parent_height = size
|
||||||
x, y = 0, 0
|
x = y = 0
|
||||||
for last, widget in loop_last(view.children):
|
app = parent.app
|
||||||
styles: Styles = widget.styles
|
for widget in parent.children:
|
||||||
|
styles = widget.styles
|
||||||
|
|
||||||
if styles.height:
|
if styles.height:
|
||||||
render_height = int(
|
render_height = int(styles.height.resolve_dimension(size, app.size))
|
||||||
styles.height.resolve_dimension(size, view.app.size)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
render_height = parent_height
|
render_height = parent_height
|
||||||
if styles.width:
|
if styles.width:
|
||||||
render_width = int(styles.width.resolve_dimension(size, view.app.size))
|
render_width = int(styles.width.resolve_dimension(size, app.size))
|
||||||
else:
|
else:
|
||||||
render_width = parent_width
|
render_width = parent_width
|
||||||
region = Region(x, y, render_width, render_height)
|
region = Region(x, y, render_width, render_height)
|
||||||
yield WidgetPlacement(region, widget, order=0)
|
add_placement(WidgetPlacement(region, widget, order=0))
|
||||||
x += render_width
|
x += render_width
|
||||||
|
|
||||||
|
return placements, set(parent.children)
|
||||||
|
|||||||
@@ -8,34 +8,35 @@ from ..layout import Layout, WidgetPlacement
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
from ..view import View
|
from ..screen import Screen
|
||||||
|
|
||||||
|
|
||||||
class VerticalLayout(Layout):
|
class VerticalLayout(Layout):
|
||||||
def get_widgets(self, view: View) -> Iterable[Widget]:
|
|
||||||
return view.children
|
|
||||||
|
|
||||||
def arrange(
|
def arrange(
|
||||||
self, view: View, size: Size, scroll: Offset
|
self, parent: Widget, size: Size, scroll: Offset
|
||||||
) -> Iterable[WidgetPlacement]:
|
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
||||||
parent_width, parent_height = size
|
|
||||||
x, y = 0, 0
|
|
||||||
|
|
||||||
for widget in view.children:
|
placements: list[WidgetPlacement] = []
|
||||||
styles: Styles = widget.styles
|
add_placement = placements.append
|
||||||
|
|
||||||
|
parent_width, parent_height = size
|
||||||
|
x = y = 0
|
||||||
|
app = parent.app
|
||||||
|
for widget in parent.children:
|
||||||
|
styles = widget.styles
|
||||||
|
|
||||||
if styles.height:
|
if styles.height:
|
||||||
render_height = int(
|
render_height = int(styles.height.resolve_dimension(size, app.size))
|
||||||
styles.height.resolve_dimension(size, view.app.size)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
render_height = size.height
|
render_height = size.height
|
||||||
|
|
||||||
if styles.width:
|
if styles.width:
|
||||||
render_width = int(styles.width.resolve_dimension(size, view.app.size))
|
render_width = int(styles.width.resolve_dimension(size, app.size))
|
||||||
else:
|
else:
|
||||||
render_width = parent_width
|
render_width = parent_width
|
||||||
|
|
||||||
region = Region(x, y, render_width, render_height)
|
region = Region(x, y, render_width, render_height)
|
||||||
yield WidgetPlacement(region, widget, 0)
|
add_placement(WidgetPlacement(region, widget, 0))
|
||||||
y += render_height
|
y += render_height
|
||||||
|
|
||||||
|
return placements, set(parent.children)
|
||||||
|
|||||||
@@ -24,8 +24,11 @@ class Message:
|
|||||||
]
|
]
|
||||||
|
|
||||||
sender: MessageTarget
|
sender: MessageTarget
|
||||||
bubble: ClassVar[bool] = True
|
bubble: ClassVar[bool] = True # Message will bubble to parent
|
||||||
verbosity: ClassVar[int] = 1
|
verbosity: ClassVar[int] = 1 # Verbosity (higher the more verbose)
|
||||||
|
system: ClassVar[
|
||||||
|
bool
|
||||||
|
] = False # Message is system related and may not be handled by client code
|
||||||
|
|
||||||
def __init__(self, sender: MessageTarget) -> None:
|
def __init__(self, sender: MessageTarget) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -45,10 +48,13 @@ class Message:
|
|||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield self.sender
|
yield self.sender
|
||||||
|
|
||||||
def __init_subclass__(cls, bubble: bool = True, verbosity: int = 1) -> None:
|
def __init_subclass__(
|
||||||
|
cls, bubble: bool = True, verbosity: int = 1, system: bool = False
|
||||||
|
) -> None:
|
||||||
super().__init_subclass__()
|
super().__init_subclass__()
|
||||||
cls.bubble = bubble
|
cls.bubble = bubble
|
||||||
cls.verbosity = verbosity
|
cls.verbosity = verbosity
|
||||||
|
cls.system = system
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_forwarded(self) -> bool:
|
def is_forwarded(self) -> bool:
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ from ._timer import Timer, TimerCallback
|
|||||||
from ._callback import invoke
|
from ._callback import invoke
|
||||||
from ._context import active_app
|
from ._context import active_app
|
||||||
from .message import Message
|
from .message import Message
|
||||||
|
from . import messages
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .app import App
|
from .app import App
|
||||||
from .view import View
|
from .screen import Screen
|
||||||
|
|
||||||
|
|
||||||
class NoParent(Exception):
|
class NoParent(Exception):
|
||||||
@@ -215,7 +216,7 @@ class MessagePump:
|
|||||||
self.app.panic()
|
self.app.panic()
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
if isinstance(message, events.Event) and self._message_queue.empty():
|
if self._message_queue.empty():
|
||||||
if not self._closed:
|
if not self._closed:
|
||||||
event = events.Idle(self)
|
event = events.Idle(self)
|
||||||
for method in self._get_dispatch_methods("on_idle", event):
|
for method in self._get_dispatch_methods("on_idle", event):
|
||||||
@@ -225,6 +226,8 @@ class MessagePump:
|
|||||||
|
|
||||||
async def dispatch_message(self, message: Message) -> bool | None:
|
async def dispatch_message(self, message: Message) -> bool | None:
|
||||||
_rich_traceback_guard = True
|
_rich_traceback_guard = True
|
||||||
|
if message.system:
|
||||||
|
return False
|
||||||
if isinstance(message, events.Event):
|
if isinstance(message, events.Event):
|
||||||
if not isinstance(message, events.Null):
|
if not isinstance(message, events.Null):
|
||||||
await self.on_event(message)
|
await self.on_event(message)
|
||||||
@@ -271,13 +274,10 @@ class MessagePump:
|
|||||||
if not self._parent._closed and not self._parent._closing:
|
if not self._parent._closed and not self._parent._closing:
|
||||||
await self._parent.post_message(message)
|
await self._parent.post_message(message)
|
||||||
|
|
||||||
def post_message_no_wait(self, message: Message) -> bool:
|
def check_idle(self):
|
||||||
if self._closing or self._closed:
|
"""Prompt the message pump to call idle if the queue is empty."""
|
||||||
return False
|
if self._message_queue.empty():
|
||||||
if not self.check_message_enabled(message):
|
self.post_message_no_wait(messages.Prompt(sender=self))
|
||||||
return True
|
|
||||||
self._message_queue.put_nowait(message)
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def post_message(self, message: Message) -> bool:
|
async def post_message(self, message: Message) -> bool:
|
||||||
if self._closing or self._closed:
|
if self._closing or self._closed:
|
||||||
@@ -287,16 +287,24 @@ class MessagePump:
|
|||||||
await self._message_queue.put(message)
|
await self._message_queue.put(message)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def post_message_from_child_no_wait(self, message: Message) -> bool:
|
def post_message_no_wait(self, message: Message) -> bool:
|
||||||
if self._closing or self._closed:
|
if self._closing or self._closed:
|
||||||
return False
|
return False
|
||||||
return self.post_message_no_wait(message)
|
if not self.check_message_enabled(message):
|
||||||
|
return True
|
||||||
|
self._message_queue.put_nowait(message)
|
||||||
|
return True
|
||||||
|
|
||||||
async def post_message_from_child(self, message: Message) -> bool:
|
async def post_message_from_child(self, message: Message) -> bool:
|
||||||
if self._closing or self._closed:
|
if self._closing or self._closed:
|
||||||
return False
|
return False
|
||||||
return await self.post_message(message)
|
return await self.post_message(message)
|
||||||
|
|
||||||
|
def post_message_from_child_no_wait(self, message: Message) -> bool:
|
||||||
|
if self._closing or self._closed:
|
||||||
|
return False
|
||||||
|
return self.post_message_no_wait(message)
|
||||||
|
|
||||||
async def on_callback(self, event: events.Callback) -> None:
|
async def on_callback(self, event: events.Callback) -> None:
|
||||||
await event.callback()
|
await event.callback()
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ if TYPE_CHECKING:
|
|||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
|
|
||||||
|
|
||||||
|
@rich.repr.auto
|
||||||
|
class Refresh(Message):
|
||||||
|
def can_replace(self, message: Message) -> bool:
|
||||||
|
return isinstance(message, Refresh)
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class Update(Message, verbosity=3):
|
class Update(Message, verbosity=3):
|
||||||
def __init__(self, sender: MessagePump, widget: Widget):
|
def __init__(self, sender: MessagePump, widget: Widget):
|
||||||
@@ -50,3 +56,10 @@ class StylesUpdated(Message):
|
|||||||
|
|
||||||
def can_replace(self, message: Message) -> bool:
|
def can_replace(self, message: Message) -> bool:
|
||||||
return isinstance(message, StylesUpdated)
|
return isinstance(message, StylesUpdated)
|
||||||
|
|
||||||
|
|
||||||
|
class Prompt(Message, system=True):
|
||||||
|
"""Used to 'wake up' an event loop."""
|
||||||
|
|
||||||
|
def can_replace(self, message: Message) -> bool:
|
||||||
|
return isinstance(message, StylesUpdated)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import rich.repr
|
|||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
|
|
||||||
from . import events, messages
|
from . import events, messages, errors
|
||||||
|
|
||||||
from .geometry import Offset, Region
|
from .geometry import Offset, Region
|
||||||
from ._compositor import Compositor
|
from ._compositor import Compositor
|
||||||
@@ -14,7 +14,7 @@ from .renderables.gradient import VerticalGradient
|
|||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class View(Widget):
|
class Screen(Widget):
|
||||||
"""A widget for the root of the app."""
|
"""A widget for the root of the app."""
|
||||||
|
|
||||||
DEFAULT_STYLES = """
|
DEFAULT_STYLES = """
|
||||||
@@ -33,7 +33,7 @@ class View(Widget):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
return VerticalGradient("#11998e", "#38ef7d")
|
return self._compositor
|
||||||
|
|
||||||
def get_offset(self, widget: Widget) -> Offset:
|
def get_offset(self, widget: Widget) -> Offset:
|
||||||
"""Get the absolute offset of a given Widget.
|
"""Get the absolute offset of a given Widget.
|
||||||
@@ -58,7 +58,7 @@ class View(Widget):
|
|||||||
"""
|
"""
|
||||||
return self._compositor.get_widget_at(x, y)
|
return self._compositor.get_widget_at(x, y)
|
||||||
|
|
||||||
def get_style_add(self, x: int, y: int) -> Style:
|
def get_style_at(self, x: int, y: int) -> Style:
|
||||||
"""Get the style under a given coordinate.
|
"""Get the style under a given coordinate.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -96,8 +96,7 @@ class View(Widget):
|
|||||||
for widget in shown:
|
for widget in shown:
|
||||||
widget.post_message_no_wait(events.Show(self))
|
widget.post_message_no_wait(events.Show(self))
|
||||||
|
|
||||||
send_resize = shown
|
send_resize = shown | resized
|
||||||
send_resize.update(resized)
|
|
||||||
|
|
||||||
for widget, region, unclipped_region in self._compositor:
|
for widget, region, unclipped_region in self._compositor:
|
||||||
widget._update_size(unclipped_region.size)
|
widget._update_size(unclipped_region.size)
|
||||||
@@ -123,12 +122,11 @@ class View(Widget):
|
|||||||
async def handle_layout(self, message: messages.Layout) -> None:
|
async def handle_layout(self, message: messages.Layout) -> None:
|
||||||
message.stop()
|
message.stop()
|
||||||
await self.refresh_layout()
|
await self.refresh_layout()
|
||||||
self.app.refresh()
|
|
||||||
|
|
||||||
async def on_resize(self, event: events.Resize) -> None:
|
async def on_resize(self, event: events.Resize) -> None:
|
||||||
event.stop()
|
|
||||||
self._update_size(event.size)
|
self._update_size(event.size)
|
||||||
await self.refresh_layout()
|
await self.refresh_layout()
|
||||||
|
event.stop()
|
||||||
|
|
||||||
async def on_idle(self, event: events.Idle) -> None:
|
async def on_idle(self, event: events.Idle) -> None:
|
||||||
if self._compositor.check_update():
|
if self._compositor.check_update():
|
||||||
@@ -143,23 +141,61 @@ class View(Widget):
|
|||||||
region = self.get_widget_region(widget)
|
region = self.get_widget_region(widget)
|
||||||
else:
|
else:
|
||||||
widget, region = self.get_widget_at(event.x, event.y)
|
widget, region = self.get_widget_at(event.x, event.y)
|
||||||
except NoWidget:
|
except errors.NoWidget:
|
||||||
await self.app.set_mouse_over(None)
|
await self.app.set_mouse_over(None)
|
||||||
else:
|
else:
|
||||||
await self.app.set_mouse_over(widget)
|
await self.app.set_mouse_over(widget)
|
||||||
await widget.forward_event(
|
mouse_event = events.MouseMove(
|
||||||
events.MouseMove(
|
self,
|
||||||
self,
|
event.x - region.x,
|
||||||
event.x - region.x,
|
event.y - region.y,
|
||||||
event.y - region.y,
|
event.delta_x,
|
||||||
event.delta_x,
|
event.delta_y,
|
||||||
event.delta_y,
|
event.button,
|
||||||
event.button,
|
event.shift,
|
||||||
event.shift,
|
event.meta,
|
||||||
event.meta,
|
event.ctrl,
|
||||||
event.ctrl,
|
screen_x=event.screen_x,
|
||||||
screen_x=event.screen_x,
|
screen_y=event.screen_y,
|
||||||
screen_y=event.screen_y,
|
style=event.style,
|
||||||
style=event.style,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
mouse_event.set_forwarded()
|
||||||
|
await widget.forward_event(mouse_event)
|
||||||
|
|
||||||
|
async def forward_event(self, event: events.Event) -> None:
|
||||||
|
if event.is_forwarded:
|
||||||
|
return
|
||||||
|
event.set_forwarded()
|
||||||
|
if isinstance(event, (events.Enter, events.Leave)):
|
||||||
|
await self.post_message(event)
|
||||||
|
|
||||||
|
elif isinstance(event, events.MouseMove):
|
||||||
|
event.style = self.get_style_at(event.screen_x, event.screen_y)
|
||||||
|
await self._on_mouse_move(event)
|
||||||
|
|
||||||
|
elif isinstance(event, events.MouseEvent):
|
||||||
|
try:
|
||||||
|
if self.app.mouse_captured:
|
||||||
|
widget = self.app.mouse_captured
|
||||||
|
region = self.get_widget_region(widget)
|
||||||
|
else:
|
||||||
|
widget, region = self.get_widget_at(event.x, event.y)
|
||||||
|
except errors.NoWidget:
|
||||||
|
await self.app.set_focus(None)
|
||||||
|
else:
|
||||||
|
if isinstance(event, events.MouseDown) and widget.can_focus:
|
||||||
|
await self.app.set_focus(widget)
|
||||||
|
event.style = self.get_style_at(event.screen_x, event.screen_y)
|
||||||
|
await widget.forward_event(event.offset(-region.x, -region.y))
|
||||||
|
|
||||||
|
elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)):
|
||||||
|
try:
|
||||||
|
widget, _region = self.get_widget_at(event.x, event.y)
|
||||||
|
except errors.NoWidget:
|
||||||
|
return
|
||||||
|
scroll_widget = widget
|
||||||
|
if scroll_widget is not None:
|
||||||
|
await scroll_widget.forward_event(event)
|
||||||
|
else:
|
||||||
|
self.log("view.forwarded", event)
|
||||||
|
await self.post_message(event)
|
||||||
@@ -127,7 +127,7 @@ class View(Widget):
|
|||||||
try:
|
try:
|
||||||
await self.layout.mount_all(self)
|
await self.layout.mount_all(self)
|
||||||
if not self.is_root_view:
|
if not self.is_root_view:
|
||||||
await self.app.view.refresh_layout()
|
await self.app.screen.refresh_layout()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.size:
|
if not self.size:
|
||||||
@@ -247,7 +247,7 @@ class View(Widget):
|
|||||||
self.log("view.forwarded", event)
|
self.log("view.forwarded", event)
|
||||||
await self.post_message(event)
|
await self.post_message(event)
|
||||||
|
|
||||||
async def action_toggle(self, name: str) -> None:
|
# async def action_toggle(self, name: str) -> None:
|
||||||
widget = self[name]
|
# widget = self[name]
|
||||||
widget.visible = not widget.display
|
# widget.visible = not widget.display
|
||||||
await self.post_message(messages.Layout(self))
|
# await self.post_message(messages.Layout(self))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from typing import cast, Optional
|
|||||||
|
|
||||||
from ..layouts.dock import DockLayout, Dock, DockEdge
|
from ..layouts.dock import DockLayout, Dock, DockEdge
|
||||||
from ..layouts.grid import GridLayout, GridAlign
|
from ..layouts.grid import GridLayout, GridAlign
|
||||||
from ..view import View
|
from ..screen import Screen
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ class DoNotSet:
|
|||||||
do_not_set = DoNotSet()
|
do_not_set = DoNotSet()
|
||||||
|
|
||||||
|
|
||||||
class DockView(View):
|
class DockView(Screen):
|
||||||
def __init__(self, name: str | None = None) -> None:
|
def __init__(self, name: str | None = None) -> None:
|
||||||
super().__init__(layout=DockLayout(), name=name)
|
super().__init__(layout=DockLayout(), name=name)
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class DockView(View):
|
|||||||
) -> GridLayout:
|
) -> GridLayout:
|
||||||
|
|
||||||
grid = GridLayout(gap=gap, gutter=gutter, align=align)
|
grid = GridLayout(gap=gap, gutter=gutter, align=align)
|
||||||
view = View(layout=grid, id=id, name=name)
|
view = Screen(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)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from ..view import View
|
from ..screen import Screen
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from ..view import View
|
from ..screen import Screen
|
||||||
from ..layouts.grid import GridLayout
|
from ..layouts.grid import GridLayout
|
||||||
|
|
||||||
|
|
||||||
class GridView(View, layout=GridLayout):
|
class GridView(Screen, layout=GridLayout):
|
||||||
@property
|
@property
|
||||||
def grid(self) -> GridLayout:
|
def grid(self) -> GridLayout:
|
||||||
assert isinstance(self.layout, GridLayout)
|
assert isinstance(self.layout, GridLayout)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from rich.console import RenderableType
|
|||||||
from .. import events
|
from .. import events
|
||||||
from ..geometry import Size, SpacingDimensions
|
from ..geometry import Size, SpacingDimensions
|
||||||
from ..layouts.vertical import VerticalLayout
|
from ..layouts.vertical import VerticalLayout
|
||||||
from ..view import View
|
from ..screen import Screen
|
||||||
from ..message import Message
|
from ..message import Message
|
||||||
from .. import messages
|
from .. import messages
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
@@ -17,7 +17,7 @@ class WindowChange(Message):
|
|||||||
return isinstance(message, WindowChange)
|
return isinstance(message, WindowChange)
|
||||||
|
|
||||||
|
|
||||||
class WindowView(View, layout=VerticalLayout):
|
class WindowView(Screen, layout=VerticalLayout):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
widget: RenderableType | Widget,
|
widget: RenderableType | Widget,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from .reactive import Reactive, watch
|
|||||||
from .renderables.opacity import Opacity
|
from .renderables.opacity import Opacity
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .view import View
|
from .screen import Screen
|
||||||
|
|
||||||
|
|
||||||
class RenderCache(NamedTuple):
|
class RenderCache(NamedTuple):
|
||||||
@@ -55,20 +55,20 @@ class RenderCache(NamedTuple):
|
|||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class Widget(DOMNode):
|
class Widget(DOMNode):
|
||||||
_counts: ClassVar[dict[str, int]] = {}
|
|
||||||
can_focus: bool = False
|
can_focus: bool = False
|
||||||
|
|
||||||
DEFAULT_STYLES = """
|
DEFAULT_STYLES = """
|
||||||
dock: _default
|
dock: _default
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
def __init__(
|
||||||
if name is None:
|
self,
|
||||||
class_name = self.__class__.__name__
|
*children: Widget,
|
||||||
Widget._counts.setdefault(class_name, 0)
|
name: str | None = None,
|
||||||
Widget._counts[class_name] += 1
|
id: str | None = None,
|
||||||
_count = self._counts[class_name]
|
classes: Iterable[str] | None = None,
|
||||||
name = f"{class_name}{_count}"
|
) -> None:
|
||||||
|
|
||||||
self._size = Size(0, 0)
|
self._size = Size(0, 0)
|
||||||
self._repaint_required = False
|
self._repaint_required = False
|
||||||
@@ -79,8 +79,11 @@ class Widget(DOMNode):
|
|||||||
self.render_cache: RenderCache | None = None
|
self.render_cache: RenderCache | None = None
|
||||||
self.highlight_style: Style | None = None
|
self.highlight_style: Style | None = None
|
||||||
|
|
||||||
super().__init__(name=name, id=id)
|
super().__init__(name=name, id=id, classes=classes)
|
||||||
|
self.add_children(*children)
|
||||||
|
|
||||||
|
has_focus = Reactive(False)
|
||||||
|
mouse_over = Reactive(False)
|
||||||
scroll_x = Reactive(0)
|
scroll_x = Reactive(0)
|
||||||
scroll_y = Reactive(0)
|
scroll_y = Reactive(0)
|
||||||
virtual_size = Reactive(Size(0, 0))
|
virtual_size = Reactive(Size(0, 0))
|
||||||
@@ -103,6 +106,8 @@ class Widget(DOMNode):
|
|||||||
"""Pseudo classes for a widget"""
|
"""Pseudo classes for a widget"""
|
||||||
if self._mouse_over:
|
if self._mouse_over:
|
||||||
yield "hover"
|
yield "hover"
|
||||||
|
if self.has_focus:
|
||||||
|
yield "focus"
|
||||||
# TODO: focus
|
# TODO: focus
|
||||||
|
|
||||||
def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None:
|
def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None:
|
||||||
@@ -148,6 +153,10 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
return renderable
|
return renderable
|
||||||
|
|
||||||
|
@property
|
||||||
|
def children(self) -> list[Widget]:
|
||||||
|
return list(self.node_list)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self) -> Size:
|
def size(self) -> Size:
|
||||||
return self._size
|
return self._size
|
||||||
@@ -165,11 +174,6 @@ class Widget(DOMNode):
|
|||||||
"""Get the current console."""
|
"""Get the current console."""
|
||||||
return active_app.get().console
|
return active_app.get().console
|
||||||
|
|
||||||
@property
|
|
||||||
def root_view(self) -> "View":
|
|
||||||
"""Return the top-most view."""
|
|
||||||
return active_app.get().view
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def animate(self) -> BoundAnimator:
|
def animate(self) -> BoundAnimator:
|
||||||
if self._animate is None:
|
if self._animate is None:
|
||||||
@@ -181,6 +185,14 @@ class Widget(DOMNode):
|
|||||||
def layout(self) -> Layout | None:
|
def layout(self) -> Layout | None:
|
||||||
return self.styles.layout
|
return self.styles.layout
|
||||||
|
|
||||||
|
def watch_mouse_over(self, value: bool) -> None:
|
||||||
|
"""Update from CSS if mouse over state changes."""
|
||||||
|
self.app.update_styles()
|
||||||
|
|
||||||
|
def watch_has_focus(self, value: bool) -> None:
|
||||||
|
"""Update from CSS if has focus state changes."""
|
||||||
|
self.app.update_styles()
|
||||||
|
|
||||||
def on_style_change(self) -> None:
|
def on_style_change(self) -> None:
|
||||||
self.clear_render_cache()
|
self.clear_render_cache()
|
||||||
|
|
||||||
@@ -218,8 +230,8 @@ class Widget(DOMNode):
|
|||||||
self._layout_required = False
|
self._layout_required = False
|
||||||
|
|
||||||
def get_style_at(self, x: int, y: int) -> Style:
|
def get_style_at(self, x: int, y: int) -> Style:
|
||||||
offset_x, offset_y = self.root_view.get_offset(self)
|
offset_x, offset_y = self.screen.get_offset(self)
|
||||||
return self.root_view.get_style_at(x + offset_x, y + offset_y)
|
return self.screen.get_style_at(x + offset_x, y + offset_y)
|
||||||
|
|
||||||
async def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
async def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
||||||
await self.app.call_later(callback, *args, **kwargs)
|
await self.app.call_later(callback, *args, **kwargs)
|
||||||
@@ -228,7 +240,7 @@ class Widget(DOMNode):
|
|||||||
event.set_forwarded()
|
event.set_forwarded()
|
||||||
await self.post_message(event)
|
await self.post_message(event)
|
||||||
|
|
||||||
def refresh(self, repaint: bool = True, layout: bool = False) -> None:
|
def refresh(self, *, repaint: bool = True, layout: bool = False) -> None:
|
||||||
"""Initiate a refresh of the widget.
|
"""Initiate a refresh of the widget.
|
||||||
|
|
||||||
This method sets an internal flag to perform a refresh, which will be done on the
|
This method sets an internal flag to perform a refresh, which will be done on the
|
||||||
@@ -244,7 +256,7 @@ class Widget(DOMNode):
|
|||||||
elif repaint:
|
elif repaint:
|
||||||
self.clear_render_cache()
|
self.clear_render_cache()
|
||||||
self._repaint_required = True
|
self._repaint_required = True
|
||||||
self.post_message_no_wait(events.Null(self))
|
self.check_idle()
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
"""Get renderable for widget.
|
"""Get renderable for widget.
|
||||||
@@ -254,9 +266,8 @@ class Widget(DOMNode):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Default displays a pretty repr in the center of the screen
|
# Default displays a pretty repr in the center of the screen
|
||||||
return Align.center(
|
|
||||||
Pretty(self, no_wrap=True, overflow="ellipsis"), vertical="middle"
|
return Align.center(self.css_identifier_styled, vertical="middle")
|
||||||
)
|
|
||||||
|
|
||||||
async def action(self, action: str, *params) -> None:
|
async def action(self, action: str, *params) -> None:
|
||||||
await self.app.action(action, self)
|
await self.app.action(action, self)
|
||||||
@@ -269,6 +280,7 @@ class Widget(DOMNode):
|
|||||||
return await super().post_message(message)
|
return await super().post_message(message)
|
||||||
|
|
||||||
async def on_resize(self, event: events.Resize) -> None:
|
async def on_resize(self, event: events.Resize) -> None:
|
||||||
|
self._update_size(event.size)
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
async def on_idle(self, event: events.Idle) -> None:
|
async def on_idle(self, event: events.Idle) -> None:
|
||||||
@@ -277,13 +289,14 @@ class Widget(DOMNode):
|
|||||||
# self.render_cache = None
|
# self.render_cache = None
|
||||||
self.reset_check_repaint()
|
self.reset_check_repaint()
|
||||||
self.reset_check_layout()
|
self.reset_check_layout()
|
||||||
await self.emit(messages.Layout(self))
|
await self.screen.post_message(messages.Layout(self))
|
||||||
elif repaint or self.check_repaint():
|
elif repaint or self.check_repaint():
|
||||||
# self.render_cache = None
|
# self.render_cache = None
|
||||||
self.reset_check_repaint()
|
self.reset_check_repaint()
|
||||||
await self.emit(messages.Update(self, self))
|
await self.emit(messages.Update(self, self))
|
||||||
|
|
||||||
async def focus(self) -> None:
|
async def focus(self) -> None:
|
||||||
|
"""Give input focus to this widget."""
|
||||||
await self.app.set_focus(self)
|
await self.app.set_focus(self)
|
||||||
|
|
||||||
async def capture_mouse(self, capture: bool = True) -> None:
|
async def capture_mouse(self, capture: bool = True) -> None:
|
||||||
@@ -315,14 +328,6 @@ class Widget(DOMNode):
|
|||||||
async def on_click(self, event: events.Click) -> None:
|
async def on_click(self, event: events.Click) -> None:
|
||||||
await self.broker_event("click", event)
|
await self.broker_event("click", event)
|
||||||
|
|
||||||
async def on_enter(self, event: events.Enter) -> None:
|
|
||||||
self._mouse_over = True
|
|
||||||
self.app.update_styles()
|
|
||||||
|
|
||||||
async def on_leave(self, event: events.Leave) -> None:
|
|
||||||
self._mouse_over = False
|
|
||||||
self.app.update_styles()
|
|
||||||
|
|
||||||
async def on_key(self, event: events.Key) -> None:
|
async def on_key(self, event: events.Key) -> None:
|
||||||
if await self.dispatch_key(event):
|
if await self.dispatch_key(event):
|
||||||
event.prevent_default()
|
event.prevent_default()
|
||||||
|
|||||||
@@ -130,6 +130,6 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
class TreeApp(App):
|
class TreeApp(App):
|
||||||
async def on_mount(self, event: events.Mount) -> None:
|
async def on_mount(self, event: events.Mount) -> None:
|
||||||
await self.view.dock(DirectoryTree("/Users/willmcgugan/projects"))
|
await self.screen.dock(DirectoryTree("/Users/willmcgugan/projects"))
|
||||||
|
|
||||||
TreeApp.run(log="textual.log")
|
TreeApp.run(log="textual.log")
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ class Placeholder(Widget, can_focus=True):
|
|||||||
style: Reactive[str] = Reactive("")
|
style: Reactive[str] = Reactive("")
|
||||||
height: Reactive[int | None] = Reactive(None)
|
height: Reactive[int | None] = Reactive(None)
|
||||||
|
|
||||||
def __init__(self, *, name: str | None = None, height: int | None = None) -> None:
|
# def __init__(self, *, name: str | None = None, height: int | None = None) -> None:
|
||||||
super().__init__(name=name)
|
# super().__init__(name=name)
|
||||||
self.height = height
|
# self.height = height
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield from super().__rich_repr__()
|
yield from super().__rich_repr__()
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ from ..message import Message
|
|||||||
from ..messages import CursorMove
|
from ..messages import CursorMove
|
||||||
from ..scrollbar import ScrollTo, ScrollBar
|
from ..scrollbar import ScrollTo, ScrollBar
|
||||||
from ..geometry import clamp
|
from ..geometry import clamp
|
||||||
from ..view import View
|
from ..screen import Screen
|
||||||
|
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|
||||||
from ..reactive import Reactive
|
from ..reactive import Reactive
|
||||||
|
|
||||||
|
|
||||||
class ScrollView(View):
|
class ScrollView(Screen):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
contents: RenderableType | Widget | None = None,
|
contents: RenderableType | Widget | None = None,
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
class TreeApp(App):
|
class TreeApp(App):
|
||||||
async def on_mount(self, event: events.Mount) -> None:
|
async def on_mount(self, event: events.Mount) -> None:
|
||||||
await self.view.dock(TreeControl("Tree Root", data="foo"))
|
await self.screen.dock(TreeControl("Tree Root", data="foo"))
|
||||||
|
|
||||||
async def handle_tree_click(self, message: TreeClick) -> None:
|
async def handle_tree_click(self, message: TreeClick) -> None:
|
||||||
if message.node.empty:
|
if message.node.empty:
|
||||||
|
|||||||
@@ -4,17 +4,4 @@ from textual.layouts.dock import DockLayout
|
|||||||
from textual.layouts.grid import GridLayout
|
from textual.layouts.grid import GridLayout
|
||||||
from textual.layouts.horizontal import HorizontalLayout
|
from textual.layouts.horizontal import HorizontalLayout
|
||||||
from textual.layouts.vertical import VerticalLayout
|
from textual.layouts.vertical import VerticalLayout
|
||||||
from textual.view import View
|
from textual.screen import Screen
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("layout_name, layout_type", [
|
|
||||||
["dock", DockLayout],
|
|
||||||
["grid", GridLayout],
|
|
||||||
["vertical", VerticalLayout],
|
|
||||||
["horizontal", HorizontalLayout],
|
|
||||||
])
|
|
||||||
def test_view_layout_get_and_set(layout_name, layout_type):
|
|
||||||
view = View()
|
|
||||||
view.layout = layout_name
|
|
||||||
assert type(view.layout) is layout_type
|
|
||||||
assert view.styles.layout is view.layout
|
|
||||||
|
|||||||
Reference in New Issue
Block a user