replace TextInput with Input

This commit is contained in:
Will McGugan
2022-10-01 14:43:46 +01:00
parent 9498a7fd43
commit e61eaf7597
11 changed files with 127 additions and 536 deletions

View File

@@ -8,7 +8,7 @@ except ImportError:
from rich.json import JSON
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.widgets import Static, TextInput
from textual.widgets import Static, Input
class DictionaryApp(App):
@@ -17,10 +17,10 @@ class DictionaryApp(App):
CSS_PATH = "dictionary.css"
def compose(self) -> ComposeResult:
yield TextInput(placeholder="Search for a word")
yield Input(placeholder="Search for a word")
yield Vertical(Static(id="results"), id="results-container")
async def on_text_input_changed(self, message: TextInput.Changed) -> None:
async def on_input_changed(self, message: Input.Changed) -> None:
"""A coroutine to handle a text changed message."""
if message.value:
# Look up the word in the background
@@ -35,7 +35,7 @@ class DictionaryApp(App):
async with httpx.AsyncClient() as client:
results = (await client.get(url)).text
if word == self.query_one(TextInput).value:
if word == self.query_one(Input).value:
self.query_one("#results", Static).update(JSON(results))

View File

@@ -2,30 +2,25 @@ Screen {
background: $panel;
}
TextInput {
Input {
dock: top;
border: tall $background;
width: 100%;
height: 1;
padding: 0 1;
margin: 1 1 0 1;
background: $boost;
}
TextInput:focus {
border: tall $accent;
margin: 1 0;
}
#results {
width: auto;
min-height: 100%;
padding: 0 1;
}
#results-container {
background: $background 50%;
margin: 1 2;
margin: 0;
height: 100%;
overflow: hidden auto;
border: tall $background;
}
#results-container:focus {
border: tall $accent;
}

View File

@@ -11,8 +11,8 @@ except ImportError:
from rich.markdown import Markdown
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.widgets import Static, TextInput
from textual.containers import Content
from textual.widgets import Static, Input
class DictionaryApp(App):
@@ -21,10 +21,10 @@ class DictionaryApp(App):
CSS_PATH = "dictionary.css"
def compose(self) -> ComposeResult:
yield TextInput(placeholder="Search for a word")
yield Vertical(Static(id="results"), id="results-container")
yield Input(placeholder="Search for a word")
yield Content(Static(id="results"), id="results-container")
async def on_text_input_changed(self, message: TextInput.Changed) -> None:
async def on_input_changed(self, message: Input.Changed) -> None:
"""A coroutine to handle a text changed message."""
if message.value:
# Look up the word in the background
@@ -39,13 +39,17 @@ class DictionaryApp(App):
async with httpx.AsyncClient() as client:
results = (await client.get(url)).json()
if word == self.query_one(TextInput).value:
if word == self.query_one(Input).value:
markdown = self.make_word_markdown(results)
self.query_one("#results", Static).update(Markdown(markdown))
def make_word_markdown(self, results: list[Any]) -> str:
def make_word_markdown(self, results: object) -> str:
"""Convert the results in to markdown."""
lines = []
if isinstance(results, dict):
lines.append(f"# {results['title']}")
lines.append(results["message"])
elif isinstance(results, list):
for result in results:
lines.append(f"# {result['word']}")
lines.append("")

View File

@@ -8,8 +8,7 @@ from textual.containers import Container, Horizontal, Vertical
from textual.reactive import Reactive
from textual.scrollbar import ScrollBarRender
from textual.widget import Widget
from textual.widgets import Button, Footer, Static, TextInput
from textual.widgets._text_input import TextWidgetBase
from textual.widgets import Button, Footer, Static, Input
VIRTUAL_SIZE = 100
WINDOW_SIZE = 10
@@ -45,7 +44,6 @@ class Bar(Widget):
self.set_class(running, "-active")
def render(self) -> RenderableType:
return ScrollBarRender(
virtual_size=VIRTUAL_SIZE,
window_size=WINDOW_SIZE,
@@ -67,9 +65,7 @@ class EasingApp(App):
def compose(self) -> ComposeResult:
self.animated_bar = Bar()
self.animated_bar.position = START_POSITION
duration_input = TextInput(
placeholder="Duration", initial="1.0", id="duration-input"
)
duration_input = Input("1.0", placeholder="Duration", id="duration-input")
self.opacity_widget = Static(
f"[b]Welcome to Textual![/]\n\n{TEXT}", id="opacity-widget"
@@ -109,7 +105,7 @@ class EasingApp(App):
self.animated_bar.position = value
self.opacity_widget.styles.opacity = 1 - value / END_POSITION
def on_text_input_changed(self, event: TextInput.Changed):
def on_input_changed(self, event: Input.Changed):
if event.sender.id == "duration-input":
new_duration = _try_float(event.value)
if new_duration is not None:

View File

@@ -42,3 +42,14 @@ class Grid(Widget):
layout: grid;
}
"""
class Content(Widget, can_focus=True, can_focus_children=False):
"""A container for content such as text."""
DEFAULT_CSS = """
Vertical {
layout: vertical;
overflow-y: auto;
}
"""

View File

@@ -354,6 +354,19 @@ class MouseEvent(InputEvent, bubble=True):
def style(self, style: Style) -> None:
self._style = style
def get_content_offset(self, widget: Widget) -> Offset | None:
"""Get offset within a widget's content area, or None if offset is not in content (i.e. padding or border).
Args:
widget (Widget): Widget receiving the event.
Returns:
Offset | None: An offset where the origin is at the top left of the content area.
"""
if self.offset not in widget.content_region:
return None
return self.offset - widget.gutter.top_left
def _apply_offset(self, x: int, y: int) -> MouseEvent:
return self.__class__(
self.sender,

View File

@@ -19,7 +19,6 @@ __all__ = [
"Placeholder",
"Pretty",
"Static",
"TextInput",
"Input",
"TextLog",
"TreeControl",

View File

@@ -8,7 +8,6 @@ from ._placeholder import Placeholder as Placeholder
from ._pretty import Pretty as Pretty
from ._static import Static as Static
from ._input import Input as Input
from ._text_input import TextInput as TextInput
from ._text_log import TextLog as TextLog
from ._tree_control import TreeControl as TreeControl
from ._welcome import Welcome as Welcome

View File

@@ -185,7 +185,7 @@ class Button(Static, can_focus=True):
if label is None:
label = self.css_identifier_styled
self.label = label
self.label = self.validate_label(label)
self.disabled = disabled
if disabled:

View File

@@ -1,21 +1,23 @@
from __future__ import annotations
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
from rich.cells import cell_len
from rich.cells import cell_len, get_character_cell_size
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
from rich.highlighter import Highlighter
from rich.segment import Segment
from rich.text import Text
from .. import events
from .._segment_tools import line_crop
from ..binding import Binding
from ..geometry import Size
from .._segment_tools import line_crop
from ..message import Message, MessageTarget
from ..reactive import reactive
from ..widget import Widget
class _InputRenderable:
"""Render the input content."""
def __init__(self, input: Input, cursor_visible: bool) -> None:
self.input = input
self.cursor_visible = cursor_visible
@@ -57,9 +59,12 @@ class Input(Widget, can_focus=True):
color: $text;
padding: 0 2;
border: tall $background;
width: auto;
width: 100%;
height: 1;
}
Input.-disabled {
opacity: 0.6;
}
Input:focus {
border: tall $accent;
}
@@ -77,6 +82,9 @@ class Input(Widget, can_focus=True):
Binding("left", "cursor_left", "cursor left"),
Binding("right", "cursor_right", "cursor right"),
Binding("backspace", "delete_left", "delete left"),
Binding("home", "home", "Home"),
Binding("end", "end", "Home"),
Binding("ctrl+d", "delete_right", "Delete"),
]
COMPONENT_CLASSES = {"input--cursor", "input--placeholder"}
@@ -87,10 +95,8 @@ class Input(Widget, can_focus=True):
cursor_position = reactive(0)
view_position = reactive(0)
placeholder = reactive("")
complete = reactive("")
width = reactive(1)
_cursor_visible = reactive(True)
password = reactive(False)
max_size: reactive[int | None] = reactive(None)
@@ -110,11 +116,13 @@ class Input(Widget, can_focus=True):
self.highlighter = highlighter
def _position_to_cell(self, position: int) -> int:
"""Convert an index within the value to cell position."""
cell_offset = cell_len(self.value[:position])
return cell_offset
@property
def _cursor_offset(self) -> int:
"""Get the cell offset of the cursor."""
offset = self._position_to_cell(self.cursor_position)
if self._cursor_at_end:
offset += 1
@@ -133,7 +141,7 @@ class Input(Widget, can_focus=True):
new_view_position = max(0, min(view_position, self.cursor_width - width))
return new_view_position
def watch_cursor_position(self, old_position: int, new_position: int) -> None:
def watch_cursor_position(self, cursor_position: int) -> None:
width = self.content_size.width
view_start = self.view_position
view_end = view_start + width
@@ -145,10 +153,13 @@ class Input(Widget, can_focus=True):
else:
self.view_position = self.view_position
async def watch_value(self, value: str) -> None:
await self.emit(self.Changed(self, value))
@property
def cursor_width(self) -> int:
"""Get the width of the input (with extra space for cursor at the end)."""
if self.placeholder and not self.value:
return cell_len(self.placeholder)
return self._position_to_cell(len(self.value)) + 1
@@ -171,7 +182,7 @@ class Input(Widget, can_focus=True):
super().__init__(sender)
self.value = value
class Updated(Message, bubble=True):
class Submitted(Message, bubble=True):
"""Value was updated via enter key or blur."""
def __init__(self, sender: MessageTarget, value: str) -> None:
@@ -180,6 +191,7 @@ class Input(Widget, can_focus=True):
@property
def _value(self) -> Text:
"""Value rendered as text."""
if self.password:
return Text("" * len(self.value), no_wrap=True, overflow="ignore")
else:
@@ -188,23 +200,20 @@ class Input(Widget, can_focus=True):
text = self.highlighter(text)
return text
@property
def _complete(self) -> Text:
return Text(self.complete, no_wrap=True, overflow="ignore")
def get_content_width(self, container: Size, viewport: Size) -> int:
return self.cursor_width
def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
return 1
def toggle_cursor(self) -> None:
def _toggle_cursor(self) -> None:
"""Toggle visibility of cursor."""
self._cursor_visible = not self._cursor_visible
def on_mount(self) -> None:
self.blink_timer = self.set_interval(
0.5,
self.toggle_cursor,
self._toggle_cursor,
pause=not (self.cursor_blink and self.has_focus),
)
@@ -220,14 +229,37 @@ class Input(Widget, can_focus=True):
self._cursor_visible = True
if self.cursor_blink:
self.blink_timer.reset()
event.prevent_default()
# Do key bindings first
if await self.handle_key(event):
event.stop()
elif event.key == "tab":
return
if event.char is not None:
elif event.is_printable:
event.stop()
assert event.char is not None
self.insert_text_at_cursor(event.char)
event.prevent_default()
def on_paste(self, event: events.Paste) -> None:
line = event.text.splitlines()[0]
self.insert_text_at_cursor(line)
def on_click(self, event: events.Click) -> None:
offset = event.get_content_offset(self)
if offset is None:
return
event.stop()
click_x = offset.x + self.view_position
cell_offset = 0
_cell_size = get_character_cell_size
for index, char in enumerate(self.value):
if cell_offset >= click_x:
self.cursor_position = index
break
cell_offset += _cell_size(char)
else:
self.cursor_position = len(self.value)
def insert_text_at_cursor(self, text: str) -> None:
if self.cursor_position > len(self.value):
@@ -246,6 +278,20 @@ class Input(Widget, can_focus=True):
def action_cursor_right(self) -> None:
self.cursor_position += 1
def action_home(self) -> None:
self.cursor_position = 0
def action_end(self) -> None:
self.cursor_position = len(self.value)
def action_delete_right(self) -> None:
value = self.value
delete_position = self.cursor_position
before = value[:delete_position]
after = value[delete_position + 1 :]
self.value = f"{before}{after}"
self.cursor_position = delete_position
def action_delete_left(self) -> None:
if self.cursor_position == len(self.value):
self.value = self.value[:-1]
@@ -257,3 +303,6 @@ class Input(Widget, can_focus=True):
after = value[delete_position + 1 :]
self.value = f"{before}{after}"
self.cursor_position = delete_position
async def action_submit(self) -> None:
await self.emit(self.Submitted(self, self.value))

View File

@@ -1,475 +0,0 @@
from __future__ import annotations
from typing import Callable
from rich.cells import cell_len
from rich.console import RenderableType
from rich.padding import Padding
from rich.text import Text
from textual import events, _clock
from textual._text_backend import TextEditorBackend
from textual.timer import Timer
from textual._types import MessageTarget
from textual.app import ComposeResult
from textual.geometry import Size, clamp
from textual.message import Message
from textual.reactive import Reactive
from textual.widget import Widget
class TextWidgetBase(Widget):
"""Base class for Widgets which support text input"""
STOP_PROPAGATE: set[str] = set()
"""Set of keybinds which will not be propagated to parent widgets"""
cursor_blink_enabled = Reactive(False)
cursor_blink_period = Reactive(0.6)
def __init__(
self,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
):
super().__init__(name=name, id=id, classes=classes)
self._editor = TextEditorBackend()
def on_key(self, event: events.Key) -> None:
key = event.key
if key == "escape":
return
changed = False
if event.char is not None and event.is_printable:
changed = self._editor.insert(event.char)
elif key == "backspace":
changed = self._editor.delete_back()
elif key == "ctrl+d":
changed = self._editor.delete_forward()
elif key == "left":
self._editor.cursor_left()
elif key == "right":
self._editor.cursor_right()
elif key == "home" or key == "ctrl+a":
self._editor.cursor_text_start()
elif key == "end" or key == "ctrl+e":
self._editor.cursor_text_end()
self.refresh(layout=True)
if changed:
self.post_message_no_wait(self.Changed(self, value=self._editor.content))
def _apply_cursor_to_text(self, display_text: Text, index: int) -> Text:
if index < 0:
return display_text
# Either write a cursor character or apply reverse style to cursor location
at_end_of_text = index == len(display_text)
at_end_of_line = index < len(display_text) and display_text.plain[index] == "\n"
if at_end_of_text or at_end_of_line:
display_text = Text.assemble(
display_text[:index],
"",
display_text[index:],
overflow="ignore",
no_wrap=True,
)
else:
display_text.stylize(
"reverse",
start=index,
end=index + 1,
)
return display_text
class Changed(Message, bubble=True):
namespace = "text_input"
def __init__(self, sender: MessageTarget, value: str) -> None:
"""Message posted when the user changes the value in a TextInput
Args:
sender (MessageTarget): Sender of the message
value (str): The value in the TextInput
"""
super().__init__(sender)
self.value = value
class TextInput(TextWidgetBase, can_focus=True):
"""Widget for inputting text
Args:
placeholder (str): The text that will be displayed when there's no content in the TextInput.
Defaults to an empty string.
initial (str): The initial value. Defaults to an empty string.
autocompleter (Callable[[str], str | None): Function which returns autocomplete suggestion
which will be displayed within the widget any time the content changes. The autocomplete
suggestion will be displayed as dim text similar to suggestion text in the zsh or fish shells.
"""
DEFAULT_CSS = """
TextInput {
width: auto;
height: 3;
padding: 1;
background: $surface;
content-align: left middle;
color: $text;
}
TextInput .text-input--placeholder {
color: $text-muted;
}
"""
COMPONENT_CLASSES = {
"text-input--placeholder",
}
def __init__(
self,
*,
placeholder: str = "",
initial: str = "",
autocompleter: Callable[[str], str | None] | None = None,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
):
super().__init__(name=name, id=id, classes=classes)
self.placeholder = placeholder
self._editor = TextEditorBackend(initial, 0)
self.visible_range: tuple[int, int] = (0, 0)
self.autocompleter = autocompleter
self._suggestion_suffix = ""
self._cursor_blink_visible = True
self._cursor_blink_timer: Timer | None = None
self._last_keypress_time: float = 0.0
if self.cursor_blink_enabled:
self._last_keypress_time = _clock.get_time_no_wait()
self._cursor_blink_timer = self.set_interval(
self.cursor_blink_period, self._toggle_cursor_visible
)
@property
def value(self) -> str:
"""Get the value from the text input widget as a string
Returns:
str: The value in the text input widget
"""
return self._editor.content
@value.setter
def value(self, value: str) -> None:
"""Update the value in the text input widget and move the cursor to the end of
the new value."""
self._editor.set_content(value)
self._editor.cursor_text_end()
self.refresh()
def get_content_width(self, container: Size, viewport: Size) -> int:
# TODO: Why does this need +2 ?
return min(cell_len(self._editor.content) + 2, container.width)
def _on_resize(self, event: events.Resize) -> None:
# Ensure the cursor remains visible when the widget is resized
self._reset_visible_range()
async def _on_click(self, event: events.Click) -> None:
"""When the user clicks on the text input, the cursor moves to the
character that was clicked on. Double-width characters makes this more
difficult."""
# If they've clicked outwith the content region (e.g. on padding), do nothing.
if not self.content_region.contains_point((event.screen_x, event.screen_y)):
return
self._cursor_blink_visible = True
start_index, end_index = self.visible_range
click_x = event.screen_x - self.content_region.x
new_cursor_index = start_index + click_x
# Convert click offset to cursor index accounting for varying cell lengths
cell_len_accumulated = 0
for index, character in enumerate(
self._editor.get_range(start_index, end_index)
):
cell_len_accumulated += cell_len(character)
if cell_len_accumulated > click_x:
new_cursor_index = start_index + index
break
new_cursor_index = clamp(new_cursor_index, 0, len(self._editor.content))
self._editor.cursor_index = new_cursor_index
self.refresh()
def _on_paste(self, event: events.Paste) -> None:
"""Handle Paste event by stripping newlines from the text, and inserting
the text at the cursor position, sliding the visible window if required."""
text = "".join(event.text.splitlines())
width_behind_cursor = self._visible_content_to_cursor_cell_len
self._editor.insert(text)
paste_cell_len = cell_len(text)
available_width = self.content_region.width
# By inserting the pasted text, how far beyond the bounds of the text input
# will we move the cursor? We need to slide the visible window along by that.
scroll_amount = paste_cell_len + width_behind_cursor - available_width + 1
if scroll_amount > 0:
self._slide_window(scroll_amount)
self.refresh()
def _slide_window(self, amount: int) -> None:
"""Slide the visible window left or right by `amount`. Negative integers move
the window left, and positive integers move the window right."""
start, end = self.visible_range
self.visible_range = start + amount, end + amount
def _reset_visible_range(self):
"""Reset our window into the editor content. Used when the widget is resized."""
available_width = self.content_region.width
# Adjust the window end such that the cursor is just off of it
new_visible_range_end = max(self._editor.cursor_index + 2, available_width)
# The visible window extends back by the width of the content region
new_visible_range_start = new_visible_range_end - available_width
# Check the cell length of the newly visible content and adjust window to accommodate
new_range = self._editor.get_range(
new_visible_range_start, new_visible_range_end
)
new_range_cell_len = cell_len(new_range)
additional_shift_required = max(0, new_range_cell_len - available_width)
self.visible_range = (
new_visible_range_start + additional_shift_required,
new_visible_range_end + additional_shift_required,
)
self.refresh()
def render(self) -> RenderableType:
# First render: Cursor at start of text, visible range goes from cursor to content region width
if not self.visible_range:
self.visible_range = (self._editor.cursor_index, self.content_region.width)
# We only show the cursor if the widget has focus
show_cursor = self.has_focus and self._cursor_blink_visible
if self._editor.content:
start, end = self.visible_range
visible_text = self._editor.get_range(start, end)
display_text = Text(visible_text, no_wrap=True, overflow="ignore")
if self._suggestion_suffix:
display_text.append(self._suggestion_suffix, "dim")
if show_cursor:
display_text = self._apply_cursor_to_text(
display_text, self._editor.cursor_index - start
)
return display_text
else:
# The user has not entered text - show the placeholder
display_text = Text(
self.placeholder,
self.get_component_rich_style("text-input--placeholder"),
no_wrap=True,
overflow="ignore",
)
if show_cursor:
display_text = self._apply_cursor_to_text(display_text, 0)
return display_text
@property
def _visible_content_to_cursor_cell_len(self) -> int:
"""The cell width of the visible content up to the cursor cell"""
start, _ = self.visible_range
visible_content_to_cursor = self._editor.get_range(
start, self._editor.cursor_index + 1
)
return cell_len(visible_content_to_cursor)
@property
def _cursor_at_right_edge(self) -> bool:
"""True if the cursor is at the right edge of the content area"""
return self._visible_content_to_cursor_cell_len == self.content_region.width
def _on_key(self, event: events.Key) -> None:
key = event.key
if key in self.STOP_PROPAGATE:
event.stop()
self._last_keypress_time = _clock.get_time_no_wait()
if self._cursor_blink_timer:
self._cursor_blink_visible = True
# Cursor location and the *codepoint* range of our view into the content
start, end = self.visible_range
cursor_index = self._editor.cursor_index
# We can scroll if the cell width of the content is greater than the content region
available_width = self.content_region.width
scrollable = cell_len(self._editor.content) >= available_width
# Check what content is visible from the editor, and how wide that content is
visible_content_to_cursor_cell_len = self._visible_content_to_cursor_cell_len
cursor_at_end = self._cursor_at_right_edge
key_cell_len = cell_len(key)
if event.is_printable:
# Check if we'll need to scroll to accommodate the new cell width after insertion.
if visible_content_to_cursor_cell_len + key_cell_len >= available_width:
self._slide_window(key_cell_len)
self._update_suggestion(event)
elif key == "enter" and self._editor.content:
self.post_message_no_wait(TextInput.Submitted(self, self._editor.content))
elif key == "right":
if (
cursor_at_end
or visible_content_to_cursor_cell_len == available_width - 1
and cell_len(self._editor.query_cursor_right() or "") == 2
):
if scrollable:
character_to_right = self._editor.query_cursor_right()
if character_to_right is not None:
cell_width_character_to_right = cell_len(character_to_right)
window_shift_amount = cell_width_character_to_right
else:
window_shift_amount = 1
self._slide_window(window_shift_amount)
if self._suggestion_suffix and self._editor.cursor_at_end:
self._editor.insert(self._suggestion_suffix)
self._suggestion_suffix = ""
self._reset_visible_range()
elif key == "left":
if cursor_index == start:
if scrollable and self._editor.query_cursor_left():
self.visible_range = (
cursor_index - 1,
cursor_index + available_width - 1,
)
else:
self.app.bell()
elif key == "backspace":
if cursor_index == start and self._editor.query_cursor_left():
self._slide_window(-1)
self._update_suggestion(event)
elif key == "ctrl+d":
self._update_suggestion(event)
elif key == "home" or key == "ctrl+a":
self.visible_range = (0, available_width)
elif key == "end" or key == "ctrl+e":
num_codepoints = len(self.value)
final_visible_codepoints = self._editor.get_range(
num_codepoints - available_width + 1,
max(num_codepoints, available_width) + 1,
)
cell_len_final_visible = cell_len(final_visible_codepoints)
# Additional shift to ensure there's space for double width character
additional_shift_required = (
max(0, cell_len_final_visible - available_width) + 2
)
if scrollable:
self.visible_range = (
num_codepoints - available_width + additional_shift_required,
max(available_width, num_codepoints) + additional_shift_required,
)
else:
self.visible_range = (0, available_width)
# We need to clamp the visible range to ensure we don't use negative indexing
start, end = self.visible_range
self.visible_range = (max(0, start), end)
def _update_suggestion(self, event: events.Key) -> None:
"""Run the autocompleter function, updating the suggestion if necessary"""
if self.autocompleter is not None:
# TODO: We shouldn't be doing the stuff below here, maybe we need to add
# a method to the editor to query an edit operation?
event.prevent_default()
super().on_key(event)
if self.value:
full_suggestion = self.autocompleter(self.value)
if full_suggestion:
suffix = full_suggestion[len(self.value) :]
self._suggestion_suffix = suffix
else:
self._suggestion_suffix = None
else:
self._suggestion_suffix = None
def _toggle_cursor_visible(self):
"""Manages the blinking of the cursor - ensuring blinking only starts when the
user hasn't pressed a key in some time"""
if (
_clock.get_time_no_wait() - self._last_keypress_time
> self.cursor_blink_period
):
self._cursor_blink_visible = not self._cursor_blink_visible
self.refresh()
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
class TextArea(Widget):
DEFAULT_CSS = """
TextArea { overflow: auto auto; height: 5; background: $primary-darken-1; }
"""
def compose(self) -> ComposeResult:
yield TextAreaChild()
class TextAreaChild(TextWidgetBase, can_focus=True):
# TODO: Not nearly ready for prime-time, but it exists to help
# model the superclass.
DEFAULT_CSS = "TextAreaChild { height: auto; background: $primary-darken-1; }"
STOP_PROPAGATE = {"tab", "shift+tab"}
def render(self) -> RenderableType:
# We only show the cursor if the widget has focus
show_cursor = self.has_focus
display_text = Text(self._editor.content, no_wrap=True)
if show_cursor:
display_text = self._apply_cursor_to_text(
display_text, self._editor.cursor_index
)
return Padding(display_text, pad=1)
def get_content_height(
self, container_size: Size, viewport_size: Size, width: int
) -> int:
return self._editor.content.count("\n") + 1 + 2
def _on_key(self, event: events.Key) -> None:
if event.key in self.STOP_PROPAGATE:
event.stop()
if event.key == "enter":
self._editor.insert("\n")
elif event.key == "tab":
self._editor.insert("\t")
elif event.key == "escape":
self.app.focused = None
def _on_focus(self, event: events.Focus) -> None:
self.refresh(layout=True)