prompt-toolkit / python-prompt-toolkit

Library for building powerful interactive command line applications in Python
https://python-prompt-toolkit.readthedocs.io/
BSD 3-Clause "New" or "Revised" License
9.1k stars 717 forks source link

Different rendering for focused or unfocused ScrollablePane #1792

Open msopacua opened 8 months ago

msopacua commented 8 months ago

The following code demonstrates a rendering difference between focused and unfocused mode:

import asyncio
from time import sleep

from prompt_toolkit.application import Application
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout import ScrollablePane, HSplit, Layout, Window
from prompt_toolkit.layout.controls import BufferControl
from prompt_toolkit.widgets import Frame, TextArea

class Runner:
    def __init__(self):
        self.buffer = Buffer()
        self.buffer_control = BufferControl(buffer=self.buffer)
        self.scrollable_pane = ScrollablePane(
            Window(self.buffer_control, wrap_lines=True), show_scrollbar=True, height=5
        )
        self.text_control = TextArea(
            prompt="Your username: ", multiline=False, password=False
        )
        self.focused_pane = Frame(self.text_control)

        root_container = HSplit([self.scrollable_pane, self.focused_pane])
        self.layout = Layout(root_container)

        self.app = Application(layout=self.layout)

    async def run(self):
        asyncio.create_task(self.update_buffer())
        kb = KeyBindings()

        @kb.add("c-q")
        def _(event) -> None:
            event.app.exit()

        self.layout.focus(self.focused_pane)
        await self.app.run_async()

    async def run_pane_focused(self):
        asyncio.create_task(self.update_buffer())
        kb = KeyBindings()

        @kb.add("c-q")
        def _(event) -> None:
            event.app.exit()

        self.layout.focus(self.scrollable_pane)
        await self.app.run_async()

    async def update_buffer(self):
        count = 0
        while count < 10:
            self.buffer.insert_text(f"New Line {count}\n")
            count += 1
            self.scrollable_pane.vertical_scroll = count
            await asyncio.sleep(1)

        self.app.exit()

if __name__ == "__main__":
    runner = Runner()
    # asyncio.run(runner.run())
    asyncio.run(runner.run_pane_focused())

Toggle the different runner methods to see the difference.

joouha commented 7 months ago

Hi,

You don't need to use ScrollablePane for this, as Windows with a BufferControl already supports scrolling.

Scrolling inside a Window is much more efficient than the ScrollablePane, as only the visible part of the output gets rendered.

I've updated your example to do scroll the window, and it works the same using both run methods:

import asyncio

from prompt_toolkit.application import Application
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout import HSplit, Layout, Window
from prompt_toolkit.layout.controls import BufferControl
from prompt_toolkit.widgets import Frame, TextArea
from prompt_toolkit.layout.margins import ScrollbarMargin

class Runner:
    def __init__(self):
        self.buffer = Buffer()
        self.buffer_control = BufferControl(buffer=self.buffer)
        self.window = Window(
            self.buffer_control,
            wrap_lines=True,
            height=5,
            right_margins=[ScrollbarMargin(display_arrows=True)],
        )
        self.text_control = TextArea(
            prompt="Your username: ", multiline=False, password=False
        )
        self.focused_pane = Frame(self.text_control)

        root_container = HSplit([self.window, self.focused_pane])
        self.layout = Layout(root_container)

        self.app = Application(layout=self.layout)

    async def run(self):
        asyncio.create_task(self.update_buffer())
        kb = KeyBindings()

        @kb.add("c-q")
        def _(event) -> None:
            event.app.exit()

        self.layout.focus(self.focused_pane)
        await self.app.run_async()

    async def run_pane_focused(self):
        asyncio.create_task(self.update_buffer())
        kb = KeyBindings()

        @kb.add("c-q")
        def _(event) -> None:
            event.app.exit()

        self.layout.focus(self.window)
        await self.app.run_async()

    async def update_buffer(self):
        count = 0
        while count < 10:
            self.buffer.insert_text(f"New Line {count}\n")
            count += 1
            self.window.vertical_scroll = count
            await asyncio.sleep(1)

        self.app.exit()

if __name__ == "__main__":
    runner = Runner()
    asyncio.run(runner.run())
    # asyncio.run(runner.run_pane_focused())