text-input-1

This commit is contained in:
Darren Burns
2022-05-11 10:11:01 +01:00
parent 231ad797d7
commit 0674f500ce
7 changed files with 262 additions and 9 deletions

View File

@@ -55,4 +55,5 @@ class BordersApp(App):
self.mount(borders=borders_view)
BordersApp.run(css_path="borders.css", log_path="textual.log")
app = BordersApp(css_path="borders.css", log_path="textual.log")
app.run()

View File

@@ -18,7 +18,9 @@ class ButtonsApp(App[str]):
self.exit(event.button.id)
app = ButtonsApp(log_path="textual.log", log_verbosity=2)
app = ButtonsApp(
log_path="textual.log", css_path="buttons.css", watch_css=True, log_verbosity=2
)
if __name__ == "__main__":
result = app.run()

42
sandbox/input.py Normal file
View File

@@ -0,0 +1,42 @@
from textual.app import App
from textual.widget import Widget
from textual.widgets.text_input import TextInput, TextInputBase
def celsius_to_fahrenheit(celsius: float) -> float:
return celsius * 1.8 + 32
def fahrenheit_to_celsius(fahrenheit: float) -> float:
return (fahrenheit - 32) / 1.8
class InputApp(App[str]):
def on_mount(self) -> None:
self.fahrenheit = TextInput(placeholder="Fahrenheit", id="fahrenheit")
self.celsius = TextInput(placeholder="Celsius", id="celsius")
self.fahrenheit.focus()
text_boxes = Widget(self.fahrenheit, self.celsius)
self.mount(inputs=text_boxes)
def handle_changed(self, event: TextInputBase.Changed) -> None:
try:
value = float(event.value)
except ValueError:
return
if event.sender == self.celsius:
fahrenheit = celsius_to_fahrenheit(value)
self.fahrenheit.current_text = f"{fahrenheit:.1f}"
elif event.sender == self.fahrenheit:
celsius = fahrenheit_to_celsius(value)
self.celsius.current_text = f"{celsius:.1f}"
app = InputApp(
log_path="textual.log", css_path="input.scss", watch_css=True, log_verbosity=2
)
if __name__ == "__main__":
result = app.run()
print(repr(result))

31
sandbox/input.scss Normal file
View File

@@ -0,0 +1,31 @@
App {
layout: dock;
docks: top=top bot=bottom;
background: $secondary;
}
Screen {
background: $secondary;
}
#fahrenheit {
width: 50%;
}
#celsius {
width: 50%;
}
#inputs {
dock: top;
layout: horizontal;
background: $primary;
height: 3;
}
#body {
dock: top;
background: $secondary;
height: 20;
}

View File

@@ -199,6 +199,11 @@ class Keys(str, Enum):
ShiftControlHome = ControlShiftHome
ShiftControlEnd = ControlShiftEnd
@classmethod
def values(cls):
"""Returns a set of all the enum values."""
return set(cls._value2member_map_.keys())
@dataclass
class Binding:

View File

@@ -15,7 +15,7 @@ class Button(Widget, can_focus=True):
"""A simple clickable button."""
CSS = """
Button {
width: auto;
height: 3;
@@ -23,22 +23,22 @@ class Button(Widget, can_focus=True):
background: $primary;
color: $text-primary;
content-align: center middle;
border: tall $primary-lighten-3;
margin: 1 0;
border: tall $primary-lighten-3;
margin: 1;
text-style: bold;
}
Button:hover {
background:$primary-darken-2;
color: $text-primary-darken-2;
border: tall $primary-lighten-1;
border: tall $primary-lighten-1;
}
App.-show-focus Button:focus {
tint: $accent 20%;
tint: $accent 20%;
}
"""
class Pressed(Message, bubble=True):

View File

@@ -0,0 +1,172 @@
from __future__ import annotations
from rich.console import RenderableType
from rich.text import Text
from textual import events
from textual._types import MessageTarget
from textual.geometry import Size
from textual.keys import Keys
from textual.message import Message
from textual.reactive import Reactive
from textual.widget import Widget
class TextInputBase(Widget):
ALLOW_PROPAGATE = {"tab", "shift+tab"}
current_text = Reactive("", layout=True)
cursor_index = Reactive(0)
class Changed(Message, bubble=True):
def __init__(self, sender: MessageTarget, value: str) -> None:
"""Message posted when the user presses the 'enter' key while
focused on a TextInput widget.
Args:
sender (MessageTarget): Sender of the message
value (str): The value in the TextInput
"""
super().__init__(sender)
self.value = value
def on_key(self, event: events.Key) -> None:
key = event.key
if key == "\x1b":
return
if key not in self.ALLOW_PROPAGATE:
event.stop()
changed = False
if key == "ctrl+h" and self.cursor_index != 0:
new_text = (
self.current_text[: self.cursor_index - 1]
+ self.current_text[self.cursor_index :]
)
self.current_text = new_text
self.cursor_index = max(0, self.cursor_index - 1)
changed = True
elif key == "ctrl+d" and self.cursor_index != len(self.current_text):
new_text = (
self.current_text[: self.cursor_index]
+ self.current_text[self.cursor_index + 1 :]
)
self.current_text = new_text
changed = True
elif key == "left":
self.cursor_index = max(0, self.cursor_index - 1)
elif key == "right":
self.cursor_index = min(len(self.current_text), self.cursor_index + 1)
elif key == "home":
self.cursor_index = 0
elif key == "end":
self.cursor_index = len(self.current_text)
elif key not in Keys.values():
new_text = (
self.current_text[: self.cursor_index]
+ key
+ self.current_text[self.cursor_index :]
)
self.current_text = new_text
self.cursor_index = min(len(self.current_text), self.cursor_index + 1)
changed = True
if changed:
self.post_message_no_wait(self.Changed(self, value=self.current_text))
class TextInput(TextInputBase, can_focus=True):
CSS = """
TextInput {
width: auto;
background: $primary;
height: 3;
padding: 0 1;
content-align: left middle;
}
TextInput:hover {
background: $primary-darken-1;
}
TextInput:focus {
background: $primary-darken-2;
border: solid $primary-lighten-1;
padding: 0;
}
App.-show-focus TextInput:focus {
tint: $accent 20%;
}
"""
class Submitted(Message, bubble=True):
def __init__(self, sender: MessageTarget, value: str) -> None:
"""Message posted when the user presses the 'enter' key while
focused on a TextInput widget.
Args:
sender (MessageTarget): Sender of the message
value (str): The value in the TextInput
"""
super().__init__(sender)
self.value = value
def __init__(
self,
placeholder: str = "",
initial: str = "",
*,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
):
super().__init__(name=name, id=id, classes=classes)
self.placeholder = placeholder
self.current_text = initial if initial else ""
def get_content_width(self, container_size: Size, viewport_size: Size) -> int:
return (
max(len(self.current_text), len(self.placeholder))
+ self.styles.gutter.width
)
def render(self) -> RenderableType:
# We only show the cursor if the widget has focus
show_cursor = self.has_focus
if self.current_text:
display_text = Text(self.current_text, no_wrap=True)
if show_cursor:
display_text = self._apply_cursor_to_text(
display_text, self.cursor_index
)
return display_text
else:
# The user has not entered text - show the placeholder
display_text = Text(self.placeholder, "dim")
if show_cursor:
display_text = self._apply_cursor_to_text(display_text, 0)
return display_text
def _apply_cursor_to_text(self, display_text: Text, index: int):
# Either write a cursor character or apply reverse style to cursor location
if index == len(display_text):
display_text += ""
else:
display_text.stylize(
"reverse",
start=index,
end=index + 1,
)
return display_text
def on_focus(self, event: events.Focus) -> None:
self.refresh(layout=True)
def on_key(self, event: events.Key) -> None:
key = event.key
if key == "enter" and self.current_text:
self.post_message_no_wait(TextInput.Submitted(self, self.current_text))