From 6618f0d27269a4f80be424844c130345267e6d0a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 5 Aug 2021 15:53:41 +0100 Subject: [PATCH] Fix for blanking bug --- examples/code_viewer.py | 37 +++++++--- src/textual/app.py | 7 -- src/textual/geometry.py | 4 +- src/textual/layout.py | 99 ++++++++++---------------- src/textual/layouts/vertical.py | 2 +- src/textual/view.py | 2 + src/textual/views/_window_view.py | 17 +++-- src/textual/widget.py | 19 +---- src/textual/widgets/_directory_tree.py | 2 +- src/textual/widgets/_scroll_view.py | 11 ++- src/textual/widgets/_static.py | 3 +- src/textual/widgets/_tree_control.py | 6 +- 12 files changed, 93 insertions(+), 116 deletions(-) diff --git a/examples/code_viewer.py b/examples/code_viewer.py index a89a8bd18..690001b32 100644 --- a/examples/code_viewer.py +++ b/examples/code_viewer.py @@ -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") diff --git a/src/textual/app.py b/src/textual/app.py index 1c30cc440..67a541430 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -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() diff --git a/src/textual/geometry.py b/src/textual/geometry.py index 89b1bd4f0..bc5a4c237 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -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: diff --git a/src/textual/layout.py b/src/textual/layout.py index 31e6edf7b..f74647ada 100644 --- a/src/textual/layout.py +++ b/src/textual/layout.py @@ -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 diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index cd778b84c..c128d7e55 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -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) diff --git a/src/textual/view.py b/src/textual/view.py index 078d82b65..b235f2c47 100644 --- a/src/textual/view.py +++ b/src/textual/view.py @@ -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) diff --git a/src/textual/views/_window_view.py b/src/textual/views/_window_view.py index 40f259afe..64e1afed2 100644 --- a/src/textual/views/_window_view.py +++ b/src/textual/views/_window_view.py @@ -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)) diff --git a/src/textual/widget.py b/src/textual/widget.py index 480b0bd51..f94ac6454 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -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 diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index d21f05aa7..cf2b87f72 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -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 diff --git a/src/textual/widgets/_scroll_view.py b/src/textual/widgets/_scroll_view.py index 49cea82e4..dcee85e64 100644 --- a/src/textual/widgets/_scroll_view.py +++ b/src/textual/widgets/_scroll_view.py @@ -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) diff --git a/src/textual/widgets/_static.py b/src/textual/widgets/_static.py index 79293419e..148a60074 100644 --- a/src/textual/widgets/_static.py +++ b/src/textual/widgets/_static.py @@ -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() diff --git a/src/textual/widgets/_tree_control.py b/src/textual/widgets/_tree_control.py index 0f2e2c130..ed2a794cc 100644 --- a/src/textual/widgets/_tree_control.py +++ b/src/textual/widgets/_tree_control.py @@ -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: