mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
new input features
This commit is contained in:
@@ -11,7 +11,7 @@ class InputApp(App):
|
||||
"""
|
||||
|
||||
def compose(self):
|
||||
yield Input("foo")
|
||||
yield Input("你123456789界", placeholder="Type something")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -112,7 +112,7 @@ class Reactive(Generic[ReactiveType]):
|
||||
self.internal_name = f"_reactive_{name}"
|
||||
default = self._default
|
||||
|
||||
if self._init and 0:
|
||||
if self._init:
|
||||
setattr(owner, f"_init_{name}", default)
|
||||
else:
|
||||
setattr(
|
||||
@@ -127,7 +127,7 @@ class Reactive(Generic[ReactiveType]):
|
||||
current_value = getattr(obj, self.internal_name, None)
|
||||
validate_function = getattr(obj, f"validate_{name}", None)
|
||||
first_set = getattr(obj, f"__first_set_{self.internal_name}", True)
|
||||
if callable(validate_function):
|
||||
if callable(validate_function) and not first_set:
|
||||
value = validate_function(value)
|
||||
if current_value != value or first_set:
|
||||
setattr(obj, f"__first_set_{self.internal_name}", False)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
||||
from rich.cells import cell_len
|
||||
from rich.highlighter import Highlighter
|
||||
from rich.segment import Segment
|
||||
from rich.text import Text
|
||||
|
||||
from .. import events
|
||||
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
|
||||
@@ -16,19 +20,32 @@ class _InputRenderable:
|
||||
self.input = input
|
||||
self.cursor_visible = cursor_visible
|
||||
|
||||
def __rich__(self) -> Text:
|
||||
def __rich_console__(
|
||||
self, console: "Console", options: "ConsoleOptions"
|
||||
) -> "RenderResult":
|
||||
|
||||
input = self.input
|
||||
result = input._value
|
||||
result.pad_right(input.cursor_width)
|
||||
if input._cursor_at_end:
|
||||
result.pad_right(1)
|
||||
cursor_style = input.get_component_rich_style("input--cursor")
|
||||
if self.cursor_visible and input.has_focus:
|
||||
cursor = input.cursor_position
|
||||
result.stylize(cursor_style, cursor, cursor + 1)
|
||||
width = input.size.width
|
||||
print(input.view_position, width)
|
||||
result = result[input.view_position : input.view_position + width]
|
||||
result.pad_right(width)
|
||||
return result
|
||||
width = input.content_size.width
|
||||
segments = list(result.render(console))
|
||||
line_length = Segment.get_line_length(segments)
|
||||
if line_length < width:
|
||||
segments = Segment.adjust_line_length(segments, width)
|
||||
line_length = width
|
||||
|
||||
line = line_crop(
|
||||
list(segments),
|
||||
input.view_position,
|
||||
input.view_position + width,
|
||||
line_length,
|
||||
)
|
||||
yield from line
|
||||
|
||||
|
||||
class Input(Widget, can_focus=True):
|
||||
@@ -37,17 +54,22 @@ class Input(Widget, can_focus=True):
|
||||
DEFAULT_CSS = """
|
||||
Input {
|
||||
background: $boost;
|
||||
color: $text;
|
||||
color: $text;
|
||||
padding: 0 2;
|
||||
border: tall $background;
|
||||
width: auto;
|
||||
height: 1;
|
||||
}
|
||||
Input:focus {
|
||||
border: tall $accent;
|
||||
border: tall $accent;
|
||||
}
|
||||
Input>.input--cursor {
|
||||
text-style: reverse;
|
||||
background: $surface;
|
||||
color: $text;
|
||||
text-style: reverse;
|
||||
}
|
||||
Input>.input--placeholder {
|
||||
color: $text-disabled;
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -57,76 +79,89 @@ class Input(Widget, can_focus=True):
|
||||
Binding("backspace", "delete_left", "delete left"),
|
||||
]
|
||||
|
||||
COMPONENT_CLASSES = {"input--cursor"}
|
||||
COMPONENT_CLASSES = {"input--cursor", "input--placeholder"}
|
||||
|
||||
cursor_blink = reactive(False)
|
||||
value = reactive("")
|
||||
cursor_blink = reactive(True)
|
||||
value = reactive("", layout=True)
|
||||
input_scroll_offset = reactive(0)
|
||||
cursor_position = reactive(0)
|
||||
view_position = reactive(0)
|
||||
placeholder = reactive("")
|
||||
|
||||
complete = reactive("")
|
||||
width = reactive(1)
|
||||
cursor_width = reactive(1, layout=True)
|
||||
|
||||
_cursor_visible = reactive(True)
|
||||
password = reactive(False)
|
||||
max_size: reactive[int | None] = reactive(None)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
value: str,
|
||||
value: str = "",
|
||||
placeholder: str = "",
|
||||
highlighter: Highlighter | None = None,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(name=name, id=id, classes=classes)
|
||||
self.value = value
|
||||
self.cursor_position = len(value)
|
||||
self.view_position = 0
|
||||
self.placeholder = placeholder
|
||||
self.highlighter = highlighter
|
||||
|
||||
def _position_to_cell(self, position: int) -> int:
|
||||
cell_index = sum(cell_len(char) for char in self.value[:position])
|
||||
return cell_index
|
||||
cell_offset = cell_len(self.value[:position])
|
||||
return cell_offset
|
||||
|
||||
def compute_cursor_width(self) -> int:
|
||||
return len(self.value) + (self.cursor_position >= len(self.value))
|
||||
@property
|
||||
def _cursor_offset(self) -> int:
|
||||
offset = self._position_to_cell(self.cursor_position)
|
||||
if self._cursor_at_end:
|
||||
offset += 1
|
||||
return offset
|
||||
|
||||
def validate_cursor_position(self, value: int) -> int:
|
||||
return max(0, value)
|
||||
@property
|
||||
def _cursor_at_end(self) -> bool:
|
||||
"""Check if the cursor is at the end"""
|
||||
return self.cursor_position >= len(self.value)
|
||||
|
||||
def validate_view_position(self, value: int) -> int:
|
||||
position = max(0, value)
|
||||
def validate_cursor_position(self, cursor_position: int) -> int:
|
||||
return min(max(0, cursor_position), len(self.value))
|
||||
|
||||
def validate_view_position(self, view_position: int) -> int:
|
||||
width = self.content_size.width
|
||||
if position > self.cursor_width:
|
||||
position = min(position, self.cursor_width - width)
|
||||
return position
|
||||
|
||||
# def validate_view_position(self, view_position: int) -> int:
|
||||
# width = self.content_size.width
|
||||
# position = min(view_position, self.cursor_position)
|
||||
# return max(max(0, position), self.cursor_width - width)
|
||||
|
||||
# def watch_cursor_width(self, value: int) -> None:
|
||||
# self.view_position = self.validate_view_position(self.view_position)
|
||||
|
||||
async def watch_value(self, value: str) -> None:
|
||||
self.width = len(value)
|
||||
await self.emit(self.Changed(self, value))
|
||||
|
||||
def watch_cursor_position(
|
||||
self, before_cursor_position: int, cursor_position: int
|
||||
) -> None:
|
||||
last = len(self.value)
|
||||
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:
|
||||
width = self.content_size.width
|
||||
print(f"{cursor_position=} {width=}")
|
||||
self.view_position = cursor_position - width
|
||||
view_start = self.view_position
|
||||
view_end = view_start + width
|
||||
cursor_offset = self._cursor_offset
|
||||
|
||||
# print(f"{self.view_position=}")
|
||||
if cursor_offset >= view_end or cursor_offset < view_start:
|
||||
view_position = cursor_offset - width // 2
|
||||
self.view_position = view_position
|
||||
else:
|
||||
self.view_position = self.view_position
|
||||
|
||||
if before_cursor_position == last or cursor_position == last:
|
||||
self.refresh(layout=True)
|
||||
@property
|
||||
def cursor_width(self) -> int:
|
||||
if self.placeholder and not self.value:
|
||||
|
||||
def render(self) -> _InputRenderable:
|
||||
return cell_len(self.placeholder)
|
||||
return self._position_to_cell(len(self.value)) + 1
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
self.view_position = self.view_position
|
||||
if not self.value:
|
||||
placeholder = Text(self.placeholder)
|
||||
placeholder.stylize(self.get_component_rich_style("input--placeholder"))
|
||||
if self.has_focus:
|
||||
cursor_style = self.get_component_rich_style("input--cursor")
|
||||
if self._cursor_visible:
|
||||
placeholder.stylize(cursor_style, 0, 1)
|
||||
return placeholder
|
||||
return _InputRenderable(self, self._cursor_visible)
|
||||
|
||||
class Changed(Message, bubble=True):
|
||||
@@ -145,11 +180,13 @@ class Input(Widget, can_focus=True):
|
||||
|
||||
@property
|
||||
def _value(self) -> Text:
|
||||
return (
|
||||
Text("•" * len(self.value), no_wrap=True, overflow="ignore")
|
||||
if self.password
|
||||
else Text(self.value, no_wrap=True, overflow="ignore")
|
||||
)
|
||||
if self.password:
|
||||
return Text("•" * len(self.value), no_wrap=True, overflow="ignore")
|
||||
else:
|
||||
text = Text(self.value, no_wrap=True, overflow="ignore")
|
||||
if self.highlighter is not None:
|
||||
text = self.highlighter(text)
|
||||
return text
|
||||
|
||||
@property
|
||||
def _complete(self) -> Text:
|
||||
@@ -175,6 +212,7 @@ class Input(Widget, can_focus=True):
|
||||
self.blink_timer.pause()
|
||||
|
||||
def on_focus(self) -> None:
|
||||
self.cursor_position = len(self.value)
|
||||
if self.cursor_blink:
|
||||
self.blink_timer.resume()
|
||||
|
||||
@@ -194,12 +232,13 @@ class Input(Widget, can_focus=True):
|
||||
def insert_text_at_cursor(self, text: str) -> None:
|
||||
if self.cursor_position > len(self.value):
|
||||
self.value += text
|
||||
self.cursor_position = len(self.value)
|
||||
else:
|
||||
value = self.value
|
||||
before = value[: self.cursor_position]
|
||||
after = value[self.cursor_position :]
|
||||
self.value = f"{before}{text}{after}"
|
||||
self.cursor_position += len(text)
|
||||
self.cursor_position += len(text)
|
||||
|
||||
def action_cursor_left(self) -> None:
|
||||
self.cursor_position -= 1
|
||||
|
||||
Reference in New Issue
Block a user