mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Fix for blanking bug
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user