Text entry widget

Hi, I was playing around Textual and Rich and I started to create a text entry widget.
I extended it from one of the tutorials sent by @lllama.

from rich.align import Align
from rich.box import DOUBLE, SIMPLE
from rich.console import RenderableType
from rich.panel import Panel
from rich.style import Style
from rich.text import Text
from textual import events
from textual.app import App
from textual.reactive import Reactive
from textual.views import GridView
from textual.widget import Widget
from textual.widgets import Button, ButtonPressed


class InputText(Widget):

    title: Reactive[RenderableType] = Reactive("")
    content: Reactive[RenderableType] = Reactive("")
    mouse_over: Reactive[RenderableType] = Reactive(False)
    # pos: Reactive[RenderableType] = Reactive(0)

    def __init__(self, title: str):
        super().__init__(title)
        self.title = title
        self.content = ""
        self.pos = len(self.content)

    def on_enter(self) -> None:
        self.mouse_over = True

    def on_leave(self) -> None:
        self.mouse_over = False

    def move_left(self):
        if self.pos > 0:
            self.pos -= 1
            self.refresh()

    def move_right(self):
        if self.pos < len(self.content):
            self.pos += 1
            self.refresh()

    def home(self):
        self.pos = 0
        self.refresh()

    def end(self):
        self.pos = len(self.content)
        self.refresh()

    def pre(self):
        if self.pos > 0:
            return self.content[0 : self.pos]
        else:
            return ""

    def at(self):
        clen = len(self.content)
        if self.pos < clen and clen > 0:
            return self.content[self.pos]
        else:
            return ""

    def post(self):
        if self.pos + 1 < len(self.content):
            return self.content[self.pos + 1 :]
        else:
            return ""

    def insert(self, value):
        self.content = f"{self.pre()}{value}{self.at()}{self.post()}"
        self.move_right()

    def backspace(self):
        if self.pos == len(self.content):
            self.pos -= 1
        self.content = f"{self.pre()}{self.post()}"

    def delete(self):
        post = self.post()
        if len(post) > 1:
            post = post[1:]
        else:
            post = ""
        self.content = f"{self.pre()}{self.at()}{post}"

    def on_key(self, event: events.Key) -> None:
        if self.mouse_over != True:
            return
        match event.key:
            case "left":
                self.move_left()
            case "right":
                self.move_right()
            case "home":
                self.home()
            case "end":
                self.end()
            case "delete":
                self.delete()
            case "ctrl+h":
                self.backspace()
            case "ctrl+@":  # Appears when I press shift
                pass
            case _:
                self.insert(event.key)

    def getformattedcontent(self):
        if self.mouse_over:
            f = f"[white]{self.pre()}[/white][blink][u][red]{self.at()}[/red][/u][/blink][white]{self.post()}[/white]"
            if self.pos == len(self.content):
                f += "[blink]_[blink]"
        else:
            f = f"{self.pre()}{self.at()}{self.post()}"
        return f

    def render(self) -> RenderableType:
        renderable = Align.left(self.getformattedcontent())
        return Panel(
            renderable,
            title="",
            title_align="center",
            height=3,
            style="bold white on rgb(50,57,50)",
            border_style=Style(color="green"),
            box=DOUBLE if self.mouse_over else SIMPLE,
        )


class MainApp(App):
    async def on_mount(self) -> None:
        grid = GridView()
        grid.grid.set_gap(1, 1)
        # Create rows / columns / areas
        grid.grid.add_column("column", repeat=2, fraction=1)
        grid.grid.add_row("row", repeat=3, size=3)
        grid.grid.add_widget(InputText("A"))
        grid.grid.add_widget(InputText("B"))

        await self.view.dock(grid)


MainApp.run()

I have some questions :smiley: :

  1. I was trying to display a cursor while editing the entry. Is there a way to display the real cursor in the widget? When I set to show the cursor, it is displayed at the end of the widget, what is understandable.
  2. I had some problems controlling focus. I had to click at the control to have focus. I was trying to have something like the tab key moving around controls. Is it something I have to implement at the App or is there something in Textual for this? The app can set focus in a control, but I didn’t find anything to do this kind of navigation. Mouse is great, but I’m trying to have a 100% keyboard experience. It is ok if I have to implement it, just asking if it already exists.
  3. I’m using Windows 11 with Windows Terminal. I got ctrl+@ when I press shift. I don’t know if this is intended. I’m just filtering it out now.

I’m using:
textual==0.1.18
rich==12.3.0
Python 3.10.2 on Windows 11

2 Likes