new input features

This commit is contained in:
Will McGugan
2022-09-30 16:53:22 +01:00
parent 9fa150906e
commit 9498a7fd43
3 changed files with 99 additions and 60 deletions

View File

@@ -11,7 +11,7 @@ class InputApp(App):
"""
def compose(self):
yield Input("foo")
yield Input("你123456789界", placeholder="Type something")
if __name__ == "__main__":

View File

@@ -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)

View File

@@ -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