mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
text-input-1
This commit is contained in:
@@ -55,4 +55,5 @@ class BordersApp(App):
|
|||||||
self.mount(borders=borders_view)
|
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()
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ class ButtonsApp(App[str]):
|
|||||||
self.exit(event.button.id)
|
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__":
|
if __name__ == "__main__":
|
||||||
result = app.run()
|
result = app.run()
|
||||||
|
|||||||
42
sandbox/input.py
Normal file
42
sandbox/input.py
Normal 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
31
sandbox/input.scss
Normal 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;
|
||||||
|
}
|
||||||
@@ -199,6 +199,11 @@ class Keys(str, Enum):
|
|||||||
ShiftControlHome = ControlShiftHome
|
ShiftControlHome = ControlShiftHome
|
||||||
ShiftControlEnd = ControlShiftEnd
|
ShiftControlEnd = ControlShiftEnd
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def values(cls):
|
||||||
|
"""Returns a set of all the enum values."""
|
||||||
|
return set(cls._value2member_map_.keys())
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Binding:
|
class Binding:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class Button(Widget, can_focus=True):
|
|||||||
"""A simple clickable button."""
|
"""A simple clickable button."""
|
||||||
|
|
||||||
CSS = """
|
CSS = """
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 3;
|
height: 3;
|
||||||
@@ -23,22 +23,22 @@ class Button(Widget, can_focus=True):
|
|||||||
background: $primary;
|
background: $primary;
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
border: tall $primary-lighten-3;
|
border: tall $primary-lighten-3;
|
||||||
|
|
||||||
margin: 1 0;
|
margin: 1;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
Button:hover {
|
Button:hover {
|
||||||
background:$primary-darken-2;
|
background:$primary-darken-2;
|
||||||
color: $text-primary-darken-2;
|
color: $text-primary-darken-2;
|
||||||
border: tall $primary-lighten-1;
|
border: tall $primary-lighten-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
App.-show-focus Button:focus {
|
App.-show-focus Button:focus {
|
||||||
tint: $accent 20%;
|
tint: $accent 20%;
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Pressed(Message, bubble=True):
|
class Pressed(Message, bubble=True):
|
||||||
|
|||||||
172
src/textual/widgets/text_input.py
Normal file
172
src/textual/widgets/text_input.py
Normal 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))
|
||||||
Reference in New Issue
Block a user