Fix for blanking bug

This commit is contained in:
Will McGugan
2021-08-05 15:53:41 +01:00
parent ae6aa78de9
commit 6618f0d272
12 changed files with 93 additions and 116 deletions

View File

@@ -1,7 +1,9 @@
import os
import sys
from rich.console import RenderableType
from rich.syntax import Syntax
from rich.traceback import Traceback
from textual import events
from textual.app import App
@@ -12,9 +14,13 @@ class MyApp(App):
"""An example of a very simple Textual App"""
async def on_load(self, event: events.Load) -> None:
"""Sent before going in to application mode."""
# Bind our basic keys
await self.bind("b", "view.toggle('sidebar')", "Toggle sidebar")
await self.bind("q", "quit", "Quit")
# Get path to show
try:
self.path = sys.argv[1]
except IndexError:
@@ -23,28 +29,43 @@ class MyApp(App):
)
async def on_mount(self, event: events.Mount) -> None:
"""Call after terminal goes in to application mode"""
# Create our widgets
# In this a scroll view for the code and a directory tree
self.body = ScrollView()
self.directory = DirectoryTree(self.path, "Code")
# Dock our widgets
await self.view.dock(Header(), edge="top")
await self.view.dock(Footer(), edge="bottom")
# Note the directory is also in a scroll view
await self.view.dock(
ScrollView(self.directory), edge="left", size=32, name="sidebar"
)
await self.view.dock(self.body, edge="top")
async def message_file_click(self, message: FileClick) -> None:
syntax = Syntax.from_path(
message.path,
line_numbers=True,
word_wrap=True,
indent_guides=True,
theme="monokai",
)
"""A message sent by the directory tree when a file is clicked."""
syntax: RenderableType
try:
# Construct a Syntax object for the path in the message
syntax = Syntax.from_path(
message.path,
line_numbers=True,
word_wrap=True,
indent_guides=True,
theme="monokai",
)
except Exception:
# Possibly a binary file
# For demonstration purposes we will show the traceback
syntax = Traceback(theme="monokai", width=None, show_locals=True)
self.app.sub_title = os.path.basename(message.path)
await self.body.update(syntax)
self.body.home()
# Run our app class
MyApp.run(title="Code Viewer", log="textual.log")

View File

@@ -333,7 +333,6 @@ class App(MessagePump):
await self.close_messages()
def refresh(self, repaint: bool = True, layout: bool = False) -> None:
log("APP REFRESH")
sync_available = os.environ.get("TERM_PROGRAM", "") != "Apple_Terminal"
if not self._closed:
console = self.console
@@ -352,13 +351,7 @@ class App(MessagePump):
if not self._closed:
console = self.console
try:
# if sync_available:
# console.file.write("\x1bP=1s\x1b\\")
# with console:
console.print(renderable)
# if sync_available:
# console.file.write("\x1bP=2s\x1b\\")
# console.file.flush()
except Exception:
self.panic()

View File

@@ -244,8 +244,8 @@ class Region(NamedTuple):
x, y, x2, y2 = self.corners
ox, oy, ox2, oy2 = other.corners
return ((x2 > ox >= x) or (x2 > ox2 > x) or (ox < x and ox2 > x2)) and (
(y2 > oy >= y) or (y2 > oy2 > y) or (oy < y and oy2 > y2)
return ((x2 > ox >= x) or (x2 > ox2 >= x) or (ox < x and ox2 >= x2)) and (
(y2 > oy >= y) or (y2 > oy2 >= y) or (oy < y and oy2 >= y2)
)
def contains(self, x: int, y: int) -> bool:

View File

@@ -87,6 +87,8 @@ class Layout(ABC):
def require_update(self) -> None:
self._require_update = True
self.reset()
self._layout_map = None
def reset_update(self) -> None:
self._require_update = False
@@ -97,7 +99,6 @@ class Layout(ABC):
# self.regions.clear()
# self._layout_map = None
@timer("reflow")
def reflow(
self, console: Console, width: int, height: int, scroll: Offset
) -> ReflowResult:
@@ -106,29 +107,14 @@ class Layout(ABC):
self.width = width
self.height = height
with timer("generate_map"):
map = self.generate_map(
console,
Size(width, height),
Region(0, 0, width, height),
scroll,
)
map = self.generate_map(
console,
Size(width, height),
Region(0, 0, width, height),
scroll,
)
self._require_update = False
# log(map.widgets)
# map = {
# widget: OrderedRegion(region + offset, order)
# for widget, (region, order, offset) in map.items()
# }
# Filter out widgets that are off screen or zero area
# map = {
# widget: map_region
# for widget, map_region in map.items()
# if map_region.region and viewport.overlaps(map_region.region)
# }
old_widgets = set() if self.map is None else set(self.map.keys())
new_widgets = set(map.keys())
# Newly visible widgets
@@ -189,12 +175,6 @@ class Layout(ABC):
for widget, (region, order, clip) in layers:
yield widget, region.intersection(clip), region
# def __reversed__(self) -> Iterable[tuple[Widget, Region]]:
# if self.map is not None:
# layers = sorted(self.map.items(), key=lambda item: item[1].order)
# for widget, (region, _order, clip) in layers:
# yield widget, region.intersection(clip), region
def get_offset(self, widget: Widget) -> Offset:
try:
return self.map[widget].region.origin
@@ -203,8 +183,8 @@ class Layout(ABC):
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
"""Get the widget under the given point or None."""
for widget, region, _ in self:
if widget.is_visual and region.contains(x, y):
for widget, cropped_region, region in self:
if widget.is_visual and cropped_region.contains(x, y):
return widget, region
raise NoWidget(f"No widget under screen coordinate ({x}, {y})")
@@ -284,8 +264,6 @@ class Layout(ABC):
continue
lines = widget._get_lines()
# width, height = region.size
# lines = Segment.set_shape(lines, width, height)
if clip in region:
yield region, clip, lines
@@ -310,7 +288,6 @@ class Layout(ABC):
line for _, line in sorted(bucket.items()) if line is not None
)
@timer("render")
def render(
self,
console: Console,
@@ -347,38 +324,34 @@ class Layout(ABC):
[_Segment(" " * width, background_style)] for _ in range(height)
]
# Go through all the renders in reverse order and fill buckets with no render
with timer("renders"):
renders = list(self._get_renders(console))
renders = list(self._get_renders(console))
with timer("chops"):
clip_y, clip_y2 = crop_region.y_extents
for region, clip, lines in chain(
renders, [(screen, screen, background_render)]
):
# clip = clip.intersection(crop_region)
render_region = region.intersection(clip)
for y, line in enumerate(lines, render_region.y):
if clip_y > y > clip_y2:
continue
# first_cut = clamp(render_region.x, clip_x, clip_x2)
# last_cut = clamp(render_region.x + render_region.width, clip_x, clip_x2)
first_cut = render_region.x
last_cut = render_region.x_max
final_cuts = [
cut for cut in cuts[y] if (last_cut >= cut >= first_cut)
]
# final_cuts = cuts[y]
clip_y, clip_y2 = crop_region.y_extents
for region, clip, lines in chain(
renders, [(screen, screen, background_render)]
):
# clip = clip.intersection(crop_region)
render_region = region.intersection(clip)
for y, line in enumerate(lines, render_region.y):
if clip_y > y > clip_y2:
continue
# first_cut = clamp(render_region.x, clip_x, clip_x2)
# last_cut = clamp(render_region.x + render_region.width, clip_x, clip_x2)
first_cut = render_region.x
last_cut = render_region.x_max
final_cuts = [cut for cut in cuts[y] if (last_cut >= cut >= first_cut)]
# final_cuts = cuts[y]
# log(final_cuts, render_region.x_extents)
if len(final_cuts) == 2:
cut_segments = [line]
else:
render_x = render_region.x
relative_cuts = [cut - render_x for cut in final_cuts]
_, *cut_segments = divide(line, relative_cuts)
for cut, segments in zip(final_cuts, cut_segments):
if chops[y][cut] is None:
chops[y][cut] = segments
# log(final_cuts, render_region.x_extents)
if len(final_cuts) == 2:
cut_segments = [line]
else:
render_x = render_region.x
relative_cuts = [cut - render_x for cut in final_cuts]
_, *cut_segments = divide(line, relative_cuts)
for cut, segments in zip(final_cuts, cut_segments):
if chops[y][cut] is None:
chops[y][cut] = segments
# Assemble the cut renders in to lists of segments
crop_x, crop_y, crop_x2, crop_y2 = crop_region.corners

View File

@@ -47,7 +47,7 @@ class VerticalLayout(Layout):
or widget.render_cache.size.width != render_width
):
widget.render_lines_free(render_width)
log("RENDERING")
assert widget.render_cache is not None
render_height = widget.render_cache.size.height
region = Region(x, y, render_width, render_height)
add_widget(widget, region - scroll, viewport)

View File

@@ -155,6 +155,8 @@ class View(Widget):
self._update_size(event.size)
if self.is_root_view:
await self.refresh_layout()
self.app.refresh()
event.stop()
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
return self.layout.get_widget_at(x, y)

View File

@@ -7,11 +7,12 @@ from ..geometry import Offset, Size
from ..layouts.vertical import VerticalLayout
from ..view import View
from ..message import Message
from ..messages import UpdateMessage
from ..widget import Widget
from ..widgets import Static
class VirtualSizeChange(Message):
class WindowChange(Message):
pass
@@ -37,11 +38,11 @@ class WindowView(View, layout=VerticalLayout):
layout.add(self.widget)
await self.refresh_layout()
self.refresh(layout=True)
await self.emit(VirtualSizeChange(self))
await self.emit(WindowChange(self))
async def watch_virtual_size(self, size: Size) -> None:
self.log("VIRTUAL SIZE CHANGE")
await self.emit(VirtualSizeChange(self))
await self.emit(WindowChange(self))
async def watch_scroll_x(self, value: int) -> None:
self.refresh(layout=True)
@@ -49,6 +50,10 @@ class WindowView(View, layout=VerticalLayout):
async def watch_scroll_y(self, value: int) -> None:
self.refresh(layout=True)
# async def on_resize(self, event: events.Resize) -> None:
# # self.layout.renders.pop(self.widget)
# self.require_repaint()
async def message_update(self, message: UpdateMessage) -> None:
self.layout.require_update()
await self.root_view.refresh_layout()
# self.app.refresh()
async def on_resize(self, event: events.Resize) -> None:
await self.emit(WindowChange(self))

View File

@@ -154,26 +154,12 @@ class Widget(MessagePump):
"""
if self.render_cache is None:
self.render_cache = self.render_lines()
self.log("RENDERING", self)
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))
# def require_layout(self) -> None:
# self._layout_required = True
# self.post_message_no_wait(events.Null(self))
def check_repaint(self) -> bool:
return self._repaint_required
@@ -207,9 +193,10 @@ class Widget(MessagePump):
layout (bool, optional): Also layout widgets in the view. Defaults to False.
"""
if layout:
self.clear_render_cache()
self._layout_required = True
elif repaint:
# self.clear_render_cache()
self.clear_render_cache()
self._repaint_required = True
self.post_message_no_wait(events.Null(self))
@@ -240,8 +227,6 @@ class Widget(MessagePump):
if self.check_layout():
self.reset_check_repaint()
self.reset_check_layout()
# await self.emit(UpdateMessage(self, self))
# await self.emit(UpdateMessage(self, self, layout=False))
await self.emit(LayoutMessage(self))
elif self.check_repaint():
self.render_cache = None

View File

@@ -74,7 +74,7 @@ class DirectoryTree(TreeControl[DirEntry]):
await node.add(entry.name, DirEntry(entry.path, entry.is_dir()))
node.loaded = True
await node.expand()
self.require_repaint()
# self.refresh(layout=True)
async def message_tree_click(self, message: TreeClick[DirEntry]) -> None:
dir_entry = message.node.data

View File

@@ -43,7 +43,7 @@ class ScrollView(View):
content="main,main", vscroll="vscroll,main", hscroll="main,hscroll"
)
layout.show_row("hscroll", False)
layout.show_row("vscroll", False)
layout.show_column("vscroll", False)
super().__init__(name=name, layout=layout)
x: Reactive[float] = Reactive(0, repaint=False)
@@ -80,7 +80,9 @@ class ScrollView(View):
self.window.scroll_y = round(new_value)
self.vscroll.position = round(new_value)
async def update(self, renderable: RenderableType) -> None:
async def update(self, renderable: RenderableType, home: bool = True) -> None:
if home:
self.home()
await self.window.update(renderable)
async def on_mount(self, event: events.Mount) -> None:
@@ -156,9 +158,6 @@ class ScrollView(View):
self.animate("x", self.target_x, duration=1, easing="out_cubic")
self.animate("y", self.target_y, duration=1, easing="out_cubic")
# async def on_resize(self, event: events.Resize) -> None:
# self.window.refresh()
async def message_scroll_up(self, message: Message) -> None:
self.page_up()
@@ -179,7 +178,7 @@ class ScrollView(View):
self.animate("x", self.target_x, speed=150, easing="out_cubic")
self.animate("y", self.target_y, speed=150, easing="out_cubic")
async def message_virtual_size_change(self, message: Message) -> None:
async def message_window_change(self, message: Message) -> None:
virtual_size = self.window.virtual_size
self.x = self.validate_x(self.x)
self.y = self.validate_y(self.y)

View File

@@ -21,7 +21,6 @@ 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)
@@ -29,4 +28,4 @@ class Static(Widget):
async def update(self, renderable: RenderableType) -> None:
self.renderable = renderable
self.require_repaint()
self.refresh()

View File

@@ -65,13 +65,14 @@ class TreeNode(Generic[NodeDataType]):
async def expand(self, expanded: bool = True) -> None:
self._expanded = expanded
self._tree.expanded = expanded
self._control.require_repaint()
self._control.refresh()
async def toggle(self) -> None:
await self.expand(not self._expanded)
async def add(self, label: TextType, data: NodeDataType) -> None:
await self._control.add(self._node_id, label, data=data)
self._control.refresh()
self._empty = False
def __rich__(self) -> RenderableType:
@@ -123,10 +124,9 @@ class TreeControl(Generic[NodeDataType], Widget):
child_tree.label = child_node
self.nodes[self._node_id] = child_node
self.require_repaint()
self.refresh()
def render(self) -> RenderableType:
log("RENDERING TREE", self)
return Padding(self._tree, self.padding)
def render_node(self, node: TreeNode[NodeDataType]) -> RenderableType: