mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
layout mechanism
This commit is contained in:
@@ -11,8 +11,8 @@ class GridTest(App):
|
||||
|
||||
grid = await self.view.dock_grid(edge="left", size=70, name="left")
|
||||
|
||||
self.view["left"].scroll_y = 5
|
||||
self.view["left"].scroll_x = 5
|
||||
# self.view["left"].scroll_y = 5
|
||||
# self.view["left"].scroll_x = 5
|
||||
|
||||
grid.add_column(fraction=1, name="left", min_size=20)
|
||||
grid.add_column(size=30, name="center")
|
||||
@@ -35,6 +35,7 @@ class GridTest(App):
|
||||
area3=Placeholder(name="area3"),
|
||||
area4=Placeholder(name="area4"),
|
||||
)
|
||||
await self.view.update_layout()
|
||||
|
||||
|
||||
GridTest.run(title="Grid Test", log="textual.log")
|
||||
|
||||
@@ -18,6 +18,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from . import events
|
||||
from .driver import Driver
|
||||
from .geometry import Size
|
||||
from ._types import MessageTarget
|
||||
from ._xterm_parser import XTermParser
|
||||
|
||||
@@ -72,7 +73,7 @@ class LinuxDriver(Driver):
|
||||
def on_terminal_resize(signum, stack) -> None:
|
||||
terminal_size = self._get_terminal_size()
|
||||
width, height = terminal_size
|
||||
event = events.Resize(self._target, width, height)
|
||||
event = events.Resize(self._target, Size(width, height))
|
||||
self.console.size = terminal_size
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._target.post_message(event),
|
||||
@@ -115,7 +116,7 @@ class LinuxDriver(Driver):
|
||||
)
|
||||
width, height = self.console.size = self._get_terminal_size()
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._target.post_message(events.Resize(self._target, width, height)),
|
||||
self._target.post_message(events.Resize(self._target, Size(width, height))),
|
||||
loop=loop,
|
||||
)
|
||||
self._key_thread.start()
|
||||
|
||||
@@ -267,7 +267,6 @@ class App(MessagePump):
|
||||
self.title = self._title
|
||||
self.require_layout()
|
||||
await self.animator.start()
|
||||
|
||||
await super().process_messages()
|
||||
log("PROCESS END")
|
||||
await self.animator.stop()
|
||||
|
||||
@@ -10,7 +10,6 @@ from .message import Message
|
||||
from ._types import MessageTarget
|
||||
from .keys import Keys
|
||||
|
||||
|
||||
MouseEventT = TypeVar("MouseEventT", bound="MouseEvent")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -53,6 +52,11 @@ class Shutdown(Event):
|
||||
pass
|
||||
|
||||
|
||||
class Repaint(Event, bubble=False):
|
||||
def can_replace(self, message: "Message") -> bool:
|
||||
return isinstance(message, Repaint)
|
||||
|
||||
|
||||
class Load(Event):
|
||||
"""
|
||||
Sent when the App is running but *before* the terminal is in application mode.
|
||||
@@ -87,27 +91,29 @@ class Action(Event, bubble=True):
|
||||
class Resize(Event):
|
||||
"""Sent when the app or widget has been resized."""
|
||||
|
||||
__slots__ = ["width", "height"]
|
||||
width: int
|
||||
height: int
|
||||
__slots__ = ["size"]
|
||||
size: Size
|
||||
|
||||
def __init__(self, sender: MessageTarget, width: int, height: int) -> None:
|
||||
def __init__(self, sender: MessageTarget, size: Size) -> None:
|
||||
"""
|
||||
Args:
|
||||
sender (MessageTarget): Event sender.
|
||||
width (int): New width in terminal cells.
|
||||
height (int): New height in terminal cells.
|
||||
"""
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.size = size
|
||||
super().__init__(sender)
|
||||
|
||||
def can_replace(self, message: "Message") -> bool:
|
||||
return isinstance(message, Resize)
|
||||
|
||||
@property
|
||||
def size(self) -> Size:
|
||||
return Size(self.width, self.height)
|
||||
def width(self) -> int:
|
||||
return self.size.width
|
||||
|
||||
@property
|
||||
def height(self) -> int:
|
||||
return self.size.height
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.RichReprResult:
|
||||
yield self.width
|
||||
|
||||
@@ -76,7 +76,7 @@ class Layout(ABC):
|
||||
self._layout_map: LayoutMap | None = None
|
||||
self.width = 0
|
||||
self.height = 0
|
||||
self.renders: dict[Widget, tuple[Region, Region, Lines]] = {}
|
||||
self.regions: dict[Widget, tuple[Region, Region]] = {}
|
||||
self._cuts: list[list[int]] | None = None
|
||||
self._require_update: bool = True
|
||||
self.background = ""
|
||||
@@ -92,9 +92,9 @@ class Layout(ABC):
|
||||
|
||||
def reset(self) -> None:
|
||||
self._cuts = None
|
||||
if self._require_update:
|
||||
self.renders.clear()
|
||||
self._layout_map = None
|
||||
# if self._require_update:
|
||||
# self.regions.clear()
|
||||
# self._layout_map = None
|
||||
|
||||
def reflow(
|
||||
self, console: Console, width: int, height: int, scroll: Offset
|
||||
@@ -137,15 +137,9 @@ class Layout(ABC):
|
||||
|
||||
# Copy renders if the size hasn't changed
|
||||
new_renders = {
|
||||
widget: (region, clip, self.renders[widget][2])
|
||||
for widget, (region, _order, clip) in map.items()
|
||||
if (
|
||||
widget in self.renders
|
||||
and self.renders[widget][0].size == region.size
|
||||
and not widget.check_repaint()
|
||||
)
|
||||
widget: (region, clip) for widget, (region, _order, clip) in map.items()
|
||||
}
|
||||
self.renders = new_renders
|
||||
self.regions = new_renders
|
||||
|
||||
# Widgets with changed size
|
||||
resized_widgets = {
|
||||
@@ -216,9 +210,9 @@ class Layout(ABC):
|
||||
widget, region = self.get_widget_at(x, y)
|
||||
except NoWidget:
|
||||
return Style.null()
|
||||
if widget not in self.renders:
|
||||
if widget not in self.regions:
|
||||
return Style.null()
|
||||
_region, clip, lines = self.renders[widget]
|
||||
lines = widget._get_lines()
|
||||
x -= region.x
|
||||
y -= region.y
|
||||
line = lines[y]
|
||||
@@ -281,39 +275,56 @@ class Layout(ABC):
|
||||
else:
|
||||
widget_regions = []
|
||||
|
||||
def render(widget: Widget, width: int, height: int) -> Lines:
|
||||
lines = console.render_lines(
|
||||
widget, console.options.update_dimensions(width, height)
|
||||
)
|
||||
return lines
|
||||
# def render(widget: Widget, width: int, height: int) -> Lines:
|
||||
# lines = console.render_lines(
|
||||
# widget, console.options.update_dimensions(width, height)
|
||||
# )
|
||||
# return lines
|
||||
|
||||
for widget, region, _order, clip in widget_regions:
|
||||
|
||||
if not widget.is_visual:
|
||||
continue
|
||||
|
||||
region_lines = self.renders.get(widget)
|
||||
if region_lines is not None:
|
||||
region, clip, lines = region_lines
|
||||
else:
|
||||
lines = render(widget, region.width, region.height)
|
||||
log("RENDERING", widget)
|
||||
lines = widget._get_lines()
|
||||
width, height = region.size
|
||||
lines = Segment.set_shape(lines, width, height)
|
||||
# assert Segment.get_shape(lines) == region.size
|
||||
if region in clip:
|
||||
self.renders[widget] = (region, clip, lines)
|
||||
yield region, clip, lines
|
||||
elif clip.overlaps(region):
|
||||
new_region = region.intersection(clip)
|
||||
delta_x = new_region.x - region.x
|
||||
delta_y = new_region.y - region.y
|
||||
self.renders[widget] = (region, clip, lines)
|
||||
splits = [delta_x, delta_x + new_region.width]
|
||||
|
||||
lines = lines[delta_y : delta_y + new_region.height]
|
||||
|
||||
divide = Segment.divide
|
||||
lines = [list(divide(line, splits))[1] for line in lines]
|
||||
yield region, clip, lines
|
||||
|
||||
# region_lines = self.regions.get(widget)
|
||||
|
||||
# if region_lines is not None:
|
||||
# region, clip, lines = region_lines
|
||||
# else:
|
||||
# lines = render(widget, region.width, region.height)
|
||||
# log("RENDERING", widget)
|
||||
# if region in clip:
|
||||
# self.regions[widget] = (region, clip, lines)
|
||||
# yield region, clip, lines
|
||||
# elif clip.overlaps(region):
|
||||
# new_region = region.intersection(clip)
|
||||
# delta_x = new_region.x - region.x
|
||||
# delta_y = new_region.y - region.y
|
||||
# self.regions[widget] = (region, clip, lines)
|
||||
# splits = [delta_x, delta_x + new_region.width]
|
||||
|
||||
# lines = lines[delta_y : delta_y + new_region.height]
|
||||
|
||||
# divide = Segment.divide
|
||||
# lines = [list(divide(line, splits))[1] for line in lines]
|
||||
# yield region, clip, lines
|
||||
|
||||
@classmethod
|
||||
def _assemble_chops(
|
||||
cls, chops: list[dict[int, list[Segment] | None]]
|
||||
@@ -347,8 +358,6 @@ class Layout(ABC):
|
||||
|
||||
crop_region = crop or Region(0, 0, self.width, self.height)
|
||||
|
||||
# clip_x, clip_y, clip_x2, clip_y2 = clip.corners
|
||||
|
||||
divide = Segment.divide
|
||||
|
||||
# Maps each cut on to a list of segments
|
||||
@@ -411,15 +420,27 @@ class Layout(ABC):
|
||||
yield self.render(console)
|
||||
|
||||
def update_widget(self, console: Console, widget: Widget) -> LayoutUpdate | None:
|
||||
if widget not in self.renders:
|
||||
|
||||
if widget not in self.regions:
|
||||
return None
|
||||
|
||||
region, clip, lines = self.renders[widget]
|
||||
new_lines = console.render_lines(
|
||||
widget, console.options.update_dimensions(region.width, region.height)
|
||||
)
|
||||
region, clip = self.regions[widget]
|
||||
|
||||
self.renders[widget] = (region, clip, new_lines)
|
||||
if not region.size:
|
||||
return None
|
||||
|
||||
widget._clear_render_cache()
|
||||
# if not region or not clip:
|
||||
# return
|
||||
|
||||
# widget._clear_render_cache()
|
||||
# widget.render_lines()
|
||||
|
||||
# new_lines = console.render_lines(
|
||||
# widget, console.options.update_dimensions(region.width, region.height)
|
||||
# )
|
||||
|
||||
# self.regions[widget] = (region, clip, new_lines)
|
||||
|
||||
update_region = region.intersection(clip)
|
||||
update_lines = self.render(console, update_region).lines
|
||||
|
||||
@@ -28,13 +28,13 @@ class LayoutMap:
|
||||
def __getitem__(self, widget: Widget) -> RenderRegion:
|
||||
return self.widgets[widget]
|
||||
|
||||
def items(self) -> ItemsView:
|
||||
def items(self) -> ItemsView[Widget, RenderRegion]:
|
||||
return self.widgets.items()
|
||||
|
||||
def keys(self) -> KeysView:
|
||||
def keys(self) -> KeysView[Widget]:
|
||||
return self.widgets.keys()
|
||||
|
||||
def values(self) -> ValuesView:
|
||||
def values(self) -> ValuesView[RenderRegion]:
|
||||
return self.widgets.values()
|
||||
|
||||
def clear(self) -> None:
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Iterable
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
|
||||
from .. import log
|
||||
from ..geometry import Offset, Region, Size
|
||||
from ..layout import Layout
|
||||
from ..layout_map import LayoutMap
|
||||
@@ -42,22 +42,25 @@ class VerticalLayout(Layout):
|
||||
map.add_widget(console, widget, region, (self.z, index), clip)
|
||||
|
||||
for widget in self._widgets:
|
||||
try:
|
||||
region, clip, lines = self.renders[widget]
|
||||
except KeyError:
|
||||
renderable = widget.render()
|
||||
lines = console.render_lines(
|
||||
renderable, console.options.update_width(render_width)
|
||||
)
|
||||
region = Region(x, y, render_width, len(lines))
|
||||
self.renders[widget] = (region - scroll, viewport, lines)
|
||||
add_widget(widget, region - scroll, viewport)
|
||||
else:
|
||||
add_widget(
|
||||
widget,
|
||||
Region(x, y, region.width, region.height) - scroll,
|
||||
clip,
|
||||
)
|
||||
y += region.height + gutter_height
|
||||
# if widget._render_cache is not None:
|
||||
# lines = widget._render_cache.lines
|
||||
# else:
|
||||
# lines = widget.render_lines(render_width).lines
|
||||
|
||||
region = Region(x, y, render_width, 100)
|
||||
add_widget(widget, region - scroll, viewport)
|
||||
|
||||
# try:
|
||||
# region, clip = self.regions[widget]
|
||||
# except KeyError:
|
||||
# lines = widget.render_lines(render_width)
|
||||
# log("***VERTICAL", len(lines))
|
||||
# region = Region(x, y, render_width, len(lines))
|
||||
# add_widget(widget, region - scroll, viewport)
|
||||
# else:
|
||||
# add_widget(
|
||||
# widget, Region(x, y, region.width, region.height) - scroll, clip
|
||||
# )
|
||||
# y += region.height + gutter_height
|
||||
|
||||
return map
|
||||
|
||||
@@ -17,28 +17,19 @@ class UpdateMessage(Message):
|
||||
self,
|
||||
sender: MessagePump,
|
||||
widget: Widget,
|
||||
offset_x: int = 0,
|
||||
offset_y: int = 0,
|
||||
reflow: bool = False,
|
||||
):
|
||||
super().__init__(sender)
|
||||
self.widget = widget
|
||||
self.offset_x = offset_x
|
||||
self.offset_y = offset_y
|
||||
self.reflow = reflow
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.RichReprResult:
|
||||
yield self.sender
|
||||
yield "widget"
|
||||
yield "offset_x", self.offset_x, 0
|
||||
yield "offset_y", self.offset_y, 0
|
||||
yield "reflow", self.reflow, False
|
||||
|
||||
def can_replace(self, message: Message) -> bool:
|
||||
return isinstance(message, UpdateMessage) and message.sender == self.sender
|
||||
return isinstance(message, UpdateMessage) and self.widget is message.widget
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class LayoutMessage(Message):
|
||||
def can_replace(self, message: Message) -> bool:
|
||||
return isinstance(message, LayoutMessage) and message.sender == self.sender
|
||||
return isinstance(message, LayoutMessage)
|
||||
|
||||
@@ -30,7 +30,6 @@ class View(Widget):
|
||||
self.layout: Layout = layout or self.layout_factory()
|
||||
self.mouse_over: Widget | None = None
|
||||
self.focused: Widget | None = None
|
||||
self.size = Size(0, 0)
|
||||
self.widgets: set[Widget] = set()
|
||||
self.named_widgets: dict[str, Widget] = {}
|
||||
self._mouse_style: Style = Style()
|
||||
@@ -127,6 +126,7 @@ class View(Widget):
|
||||
async def message_update(self, message: UpdateMessage) -> None:
|
||||
widget = message.widget
|
||||
assert isinstance(widget, Widget)
|
||||
|
||||
display_update = self.root_view.layout.update_widget(self.console, widget)
|
||||
if display_update is not None:
|
||||
self.app.display(display_update)
|
||||
@@ -159,8 +159,12 @@ class View(Widget):
|
||||
hidden, shown, resized = self.layout.reflow(
|
||||
self.console, width, height, self.scroll
|
||||
)
|
||||
assert self.layout.map is not None
|
||||
self.virtual_size = self.layout.map.virtual_size
|
||||
# self.app.refresh()
|
||||
# for widget, region in self.layout:
|
||||
# widget._update_size(region.size)
|
||||
|
||||
self.app.refresh()
|
||||
|
||||
for widget in hidden:
|
||||
widget.post_message_no_wait(events.Hide(self))
|
||||
@@ -172,13 +176,13 @@ class View(Widget):
|
||||
|
||||
for widget, region in self.layout:
|
||||
if widget in send_resize:
|
||||
widget.post_message_no_wait(
|
||||
events.Resize(self, region.width, region.height)
|
||||
)
|
||||
widget._update_size(region.size)
|
||||
widget.post_message_no_wait(events.Resize(self, region.size))
|
||||
|
||||
async def on_resize(self, event: events.Resize) -> None:
|
||||
self.size = Size(event.width, event.height)
|
||||
await self.refresh_layout()
|
||||
self._update_size(event.size)
|
||||
if self.is_root_view:
|
||||
await self.refresh_layout()
|
||||
|
||||
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
|
||||
return self.layout.get_widget_at(x, y)
|
||||
|
||||
@@ -41,6 +41,6 @@ class WindowView(View, layout=VerticalLayout):
|
||||
async def watch_virtual_size(self, size: Size) -> None:
|
||||
await self.emit(VirtualSizeChange(self))
|
||||
|
||||
# async def on_resize(self, event: events.Resize) -> None:
|
||||
# self.layout.renders.pop(self.widget)
|
||||
# self.require_repaint()
|
||||
async def on_resize(self, event: events.Resize) -> None:
|
||||
# self.layout.renders.pop(self.widget)
|
||||
self.require_repaint()
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
ClassVar,
|
||||
NamedTuple,
|
||||
NewType,
|
||||
cast,
|
||||
)
|
||||
@@ -15,6 +16,7 @@ from rich.align import Align
|
||||
from rich.console import Console, RenderableType
|
||||
from rich.panel import Panel
|
||||
from rich.pretty import Pretty
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
|
||||
from . import events
|
||||
@@ -25,6 +27,7 @@ from .message import Message
|
||||
from .message_pump import MessagePump
|
||||
from .messages import LayoutMessage, UpdateMessage
|
||||
from .reactive import Reactive, watch
|
||||
from ._types import Lines
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .app import App
|
||||
@@ -33,6 +36,11 @@ if TYPE_CHECKING:
|
||||
log = getLogger("rich")
|
||||
|
||||
|
||||
class RenderCache(NamedTuple):
|
||||
size: Size
|
||||
lines: Lines
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class Widget(MessagePump):
|
||||
_id: ClassVar[int] = 0
|
||||
@@ -47,12 +55,12 @@ class Widget(MessagePump):
|
||||
|
||||
self.name = name or f"{class_name}#{_count}"
|
||||
|
||||
self.size = Size(0, 0)
|
||||
self.size_changed = False
|
||||
self._size = Size(0, 0)
|
||||
self._repaint_required = False
|
||||
self._layout_required = False
|
||||
self._animate: BoundAnimator | None = None
|
||||
self._reactive_watches: dict[str, Callable] = {}
|
||||
self._render_cache: RenderCache | None = None
|
||||
self.highlight_style: Style | None = None
|
||||
|
||||
super().__init__()
|
||||
@@ -83,6 +91,10 @@ class Widget(MessagePump):
|
||||
def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None:
|
||||
watch(self, attribute_name, callback)
|
||||
|
||||
@property
|
||||
def size(self) -> Size:
|
||||
return self._size
|
||||
|
||||
@property
|
||||
def is_visual(self) -> bool:
|
||||
return True
|
||||
@@ -109,11 +121,46 @@ class Widget(MessagePump):
|
||||
"""Get the layout offset as a tuple."""
|
||||
return (round(self.layout_offset_x), round(self.layout_offset_y))
|
||||
|
||||
def _update_size(self, size: Size) -> None:
|
||||
self._size = size
|
||||
# if self._render_cache and self._render_cache.size != size:
|
||||
# self.render_lines()
|
||||
# self.require_repaint()
|
||||
# self.size = size
|
||||
|
||||
def render_lines(self) -> RenderCache:
|
||||
width, height = self.size
|
||||
renderable = self.render()
|
||||
options = self.console.options.update_dimensions(width, height)
|
||||
lines = self.console.render_lines(renderable, options)
|
||||
self._render_cache = RenderCache(self.size, lines)
|
||||
return self._render_cache
|
||||
|
||||
def _get_lines(self) -> Lines:
|
||||
"""Get render lines for given dimensions.
|
||||
|
||||
Args:
|
||||
width (int): [description]
|
||||
height (int): [description]
|
||||
|
||||
Returns:
|
||||
Lines: [description]
|
||||
"""
|
||||
if self._render_cache is None:
|
||||
self._render_cache = self.render_lines()
|
||||
lines = self._render_cache.lines
|
||||
|
||||
return lines
|
||||
|
||||
def _clear_render_cache(self) -> None:
|
||||
self._render_cache = None
|
||||
|
||||
def require_repaint(self) -> None:
|
||||
"""Mark widget as requiring a repaint.
|
||||
|
||||
Actual repaint is done by parent on idle.
|
||||
"""
|
||||
self._render_cache = None
|
||||
self._repaint_required = True
|
||||
self.post_message_no_wait(events.Null(self))
|
||||
|
||||
@@ -173,13 +220,15 @@ class Widget(MessagePump):
|
||||
return True
|
||||
return await super().post_message(message)
|
||||
|
||||
async def on_event(self, event: events.Event) -> None:
|
||||
if isinstance(event, events.Resize):
|
||||
new_size = Size(event.width, event.height)
|
||||
if self.size != new_size:
|
||||
self.size = new_size
|
||||
self.require_repaint()
|
||||
await super().on_event(event)
|
||||
# async def on_event(self, event: events.Event) -> None:
|
||||
# if isinstance(event, events.Resize):
|
||||
# if self.size != event.size:
|
||||
# # self.size = event.size
|
||||
# self.require_repaint()
|
||||
# await super().on_event(event)
|
||||
|
||||
async def on_resize(self, event: events.Resize) -> None:
|
||||
self.render_lines()
|
||||
|
||||
async def on_idle(self, event: events.Idle) -> None:
|
||||
if self.check_layout():
|
||||
@@ -215,6 +264,10 @@ class Widget(MessagePump):
|
||||
if key_method is not None:
|
||||
await key_method()
|
||||
|
||||
# async def on_repaint(self) -> None:
|
||||
# if self._render_cache is None or self._render_cache.size != self.size:
|
||||
# self._render_cache = self.render_lines()
|
||||
|
||||
async def on_mouse_down(self, event: events.MouseUp) -> None:
|
||||
await self.broker_event("mouse.down", event)
|
||||
|
||||
|
||||
@@ -158,9 +158,8 @@ class ScrollView(View):
|
||||
self.animate("y", self.target_y, duration=1, easing="out_cubic")
|
||||
|
||||
async def on_resize(self, event: events.Resize) -> None:
|
||||
return
|
||||
if self.fluid:
|
||||
self.window.update()
|
||||
|
||||
self.window.require_repaint()
|
||||
|
||||
async def message_scroll_up(self, message: Message) -> None:
|
||||
self.page_up()
|
||||
@@ -185,7 +184,7 @@ class ScrollView(View):
|
||||
async def message_virtual_size_change(self, message: Message) -> None:
|
||||
|
||||
virtual_size = self.window.virtual_size
|
||||
# self.log("VIRTUAL_SIZE", self.size, virtual_size)
|
||||
self.log("VIRTUAL_SIZE", self.size, virtual_size)
|
||||
self.x = self.validate_x(self.x)
|
||||
self.y = self.validate_y(self.y)
|
||||
self.log(self.y)
|
||||
|
||||
@@ -21,6 +21,7 @@ class Static(Widget):
|
||||
self.padding = padding
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
self.log("RENDERING", self.renderable)
|
||||
renderable = self.renderable
|
||||
if self.padding:
|
||||
renderable = Padding(renderable, self.padding)
|
||||
|
||||
Reference in New Issue
Block a user