holoviz / panel

Panel: The powerful data exploration & web app framework for Python
https://panel.holoviz.org
BSD 3-Clause "New" or "Revised" License
4.4k stars 484 forks source link

Textual Reference Docs issues #6606

Open MarcSkovMadsen opened 3 months ago

MarcSkovMadsen commented 3 months ago

I tried reading about the new Textual pane. There are some obvious issues

Does not make sense to make example runnable in Pyodide

image

If you run it, you see

image

pn is not defined

The docs currently show

image

philippjfr commented 3 months ago

Textual cannot work in the static docs both as the simple export or in pyodide:

  1. To render the output the async event loop has to be started and bi-directional communication between the terminal and the backend has to occur, this is not possible in a static export.
  2. In pyodide no terminal emulation is available so it cannot work either.

We probably just need to disable pyodide and somehow embed images in the doc build.

MarcSkovMadsen commented 3 months ago

Textual cannot work in the static docs both as the simple export or in pyodide:

  1. To render the output the async event loop has to be started and bi-directional communication between the terminal and the backend has to occur, this is not possible in a static export.
  2. In pyodide no terminal emulation is available so it cannot work either.

We probably just need to disable pyodide and somehow embed images in the doc build.

Exactly.

MarcSkovMadsen commented 3 months ago

We also need to improve the code. The big example cannot be easily replicated by users because it loads a tcss file they do not know where to find.

image

image

MarcSkovMadsen commented 3 months ago

For some unknown reason it also shows a rectangle and a w above the calculator when used in a `FastListTemplate?

image

import panel as pn
import pathlib

from decimal import Decimal

from textual import events, on
from textual.app import App, ComposeResult
from textual.containers import Container
from textual.css.query import NoMatches
from textual.reactive import var
from textual.widgets import Button, Digits

from pathlib import Path
import requests

def _download_file_if_not_exists(url: str, local_path: str) -> Path:
    local_file_path = Path(local_path)

    if not local_file_path.exists():
        response = requests.get(url)
        response.raise_for_status()
        local_file_path.write_bytes(response.content)

    return local_file_path

file_url = "https://raw.githubusercontent.com/holoviz/panel/main/examples/assets/calculator.tcss"
local_file_path = "calculator.tcss"
calculator_tcss = _download_file_if_not_exists(file_url, local_file_path)

class CalculatorApp(App):
    """A working 'desktop' calculator."""

    CSS_PATH = calculator_tcss.absolute()

    numbers = var("0")
    show_ac = var(True)
    left = var(Decimal("0"))
    right = var(Decimal("0"))
    value = var("")
    operator = var("plus")

    NAME_MAP = {
        "asterisk": "multiply",
        "slash": "divide",
        "underscore": "plus-minus",
        "full_stop": "point",
        "plus_minus_sign": "plus-minus",
        "percent_sign": "percent",
        "equals_sign": "equals",
        "minus": "minus",
        "plus": "plus",
    }

    def watch_numbers(self, value: str) -> None:
        """Called when numbers is updated."""
        self.query_one("#numbers", Digits).update(value)

    def compute_show_ac(self) -> bool:
        """Compute switch to show AC or C button"""
        return self.value in ("", "0") and self.numbers == "0"

    def watch_show_ac(self, show_ac: bool) -> None:
        """Called when show_ac changes."""
        self.query_one("#c").display = not show_ac
        self.query_one("#ac").display = show_ac

    def compose(self) -> ComposeResult:
        """Add our buttons."""
        with Container(id="calculator"):
            yield Digits(id="numbers")
            yield Button("AC", id="ac", variant="primary")
            yield Button("C", id="c", variant="primary")
            yield Button("+/-", id="plus-minus", variant="primary")
            yield Button("%", id="percent", variant="primary")
            yield Button("÷", id="divide", variant="warning")
            yield Button("7", id="number-7", classes="number")
            yield Button("8", id="number-8", classes="number")
            yield Button("9", id="number-9", classes="number")
            yield Button("×", id="multiply", variant="warning")
            yield Button("4", id="number-4", classes="number")
            yield Button("5", id="number-5", classes="number")
            yield Button("6", id="number-6", classes="number")
            yield Button("-", id="minus", variant="warning")
            yield Button("1", id="number-1", classes="number")
            yield Button("2", id="number-2", classes="number")
            yield Button("3", id="number-3", classes="number")
            yield Button("+", id="plus", variant="warning")
            yield Button("0", id="number-0", classes="number")
            yield Button(".", id="point")
            yield Button("=", id="equals", variant="warning")

    def on_key(self, event: events.Key) -> None:
        """Called when the user presses a key."""

        def press(button_id: str) -> None:
            """Press a button, should it exist."""
            try:
                self.query_one(f"#{button_id}", Button).press()
            except NoMatches:
                pass

        key = event.key
        if key.isdecimal():
            press(f"number-{key}")
        elif key == "c":
            press("c")
            press("ac")
        else:
            button_id = self.NAME_MAP.get(key)
            if button_id is not None:
                press(self.NAME_MAP.get(key, key))

    @on(Button.Pressed, ".number")
    def number_pressed(self, event: Button.Pressed) -> None:
        """Pressed a number."""
        assert event.button.id is not None
        number = event.button.id.partition("-")[-1]
        self.numbers = self.value = self.value.lstrip("0") + number

    @on(Button.Pressed, "#plus-minus")
    def plus_minus_pressed(self) -> None:
        """Pressed + / -"""
        self.numbers = self.value = str(Decimal(self.value or "0") * -1)

    @on(Button.Pressed, "#percent")
    def percent_pressed(self) -> None:
        """Pressed %"""
        self.numbers = self.value = str(Decimal(self.value or "0") / Decimal(100))

    @on(Button.Pressed, "#point")
    def pressed_point(self) -> None:
        """Pressed ."""
        if "." not in self.value:
            self.numbers = self.value = (self.value or "0") + "."

    @on(Button.Pressed, "#ac")
    def pressed_ac(self) -> None:
        """Pressed AC"""
        self.value = ""
        self.left = self.right = Decimal(0)
        self.operator = "plus"
        self.numbers = "0"

    @on(Button.Pressed, "#c")
    def pressed_c(self) -> None:
        """Pressed C"""
        self.value = ""
        self.numbers = "0"

    def _do_math(self) -> None:
        """Does the math: LEFT OPERATOR RIGHT"""
        try:
            if self.operator == "plus":
                self.left += self.right
            elif self.operator == "minus":
                self.left -= self.right
            elif self.operator == "divide":
                self.left /= self.right
            elif self.operator == "multiply":
                self.left *= self.right
            self.numbers = str(self.left)
            self.value = ""
        except Exception:
            self.numbers = "Error"

    @on(Button.Pressed, "#plus,#minus,#divide,#multiply")
    def pressed_op(self, event: Button.Pressed) -> None:
        """Pressed one of the arithmetic operations."""
        self.right = Decimal(self.value or "0")
        self._do_math()
        assert event.button.id is not None
        self.operator = event.button.id

    @on(Button.Pressed, "#equals")
    def pressed_equals(self) -> None:
        """Pressed ="""
        if self.value:
            self.right = Decimal(self.value)
        self._do_math()

calculator = CalculatorApp()
textual_pane = pn.pane.Textual(calculator, height=600, width=400)

pn.template.FastListTemplate(
    site="Panel", title="Textual", main=[textual_pane], theme="dark", main_max_width="610px", main_layout=None, theme_toggle=False
).servable();
MarcSkovMadsen commented 3 months ago

Seems this CSS should be added

CSS = """
textarea.xterm-helper-textarea, span.xterm-char-measure-element {
    pointer-events: none;
    opacity: 0;
}
"""
MarcSkovMadsen commented 3 months ago

All of the above issues except that the reference guide is rendered with pyodide are solved in #6614. I expect someone else to fix the pyodide issue.

philippjfr commented 2 months ago

Potentially fixed in https://github.com/holoviz/panel/pull/6672 but will confirm in a new dev release.