Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Text input improvements #528

Merged
merged 29 commits into from
May 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
353a31f
Basic suggestions
darrenburns May 17, 2022
3cc458e
Suggestion completions
darrenburns May 17, 2022
bca63fe
Merge branch 'text-input' into text-input-suggest
darrenburns May 18, 2022
66b7e34
Suggestion autocomplete scrolls if required
darrenburns May 18, 2022
71a7286
Cursor blinking
darrenburns May 18, 2022
d8179c7
Conditional blinking
darrenburns May 18, 2022
3be4e56
Enable cursor blinking
darrenburns May 18, 2022
20d6515
Remove unused print
darrenburns May 18, 2022
307ea23
Click on ASCII text in text input to move cursor
darrenburns May 18, 2022
6a56e77
Calculating offset correctly
darrenburns May 18, 2022
bc7a64d
Fix scrolling through double-width Unicode characters
darrenburns May 19, 2022
3bca274
TextInput debugging keybind
darrenburns May 19, 2022
754cc0b
Scrolling on text insertion of non-single cell chars
darrenburns May 19, 2022
e9535d2
Text input click support with double width characters
darrenburns May 19, 2022
8bf8db5
Fix end key double-width char support, fix indexing issue where curso…
darrenburns May 20, 2022
5b0f7a9
Keep cursor visible on TextInput widget size change
darrenburns May 20, 2022
ad2a5e1
Better support for double width characters
darrenburns May 20, 2022
5b608bf
Handle home and end ctrl+a and ctrl+e
darrenburns May 20, 2022
66d73bc
Merge branch 'css' of github.com:Textualize/textual into text-input-c…
darrenburns May 20, 2022
d8aa103
Simple file search live filter sandbox example
darrenburns May 20, 2022
4c03fb0
Simplify file search sandbox
darrenburns May 20, 2022
4a4841d
Suggestion autocomplete function now returns full suggestion, Textual…
darrenburns May 20, 2022
bdef4da
The suggestion is applied to the whole value, not just the visible co…
darrenburns May 20, 2022
ff5eaa0
Merge branch 'css' of github.com:Textualize/textual into text-input-c…
darrenburns May 20, 2022
1fbf943
Add some docstrings
darrenburns May 20, 2022
c5c6e20
Reorder if-elifs in text input key event handler
darrenburns May 20, 2022
858e718
Ensure enough space for double-width on resize
darrenburns May 24, 2022
b0e197f
Merge branch 'css' of github.com:Textualize/textual into text-input-c…
darrenburns May 25, 2022
2d7f0b1
Use clock.get_time_no_wait instead of time.monotonic
darrenburns May 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions sandbox/file_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from __future__ import annotations

from pathlib import Path
from typing import Iterable

from rich.console import RenderableType
from rich.style import Style
from rich.table import Table
from rich.text import Text

from textual.app import App
from textual.geometry import Size
from textual.reactive import Reactive
from textual.widget import Widget
from textual.widgets.text_input import TextInput, TextWidgetBase


def get_files() -> list[Path]:
files = list(Path.cwd().iterdir())
return files


class FileTable(Widget):
filter = Reactive("", layout=True)

def __init__(self, *args, files: Iterable[Path] | None = None, **kwargs):
super().__init__(*args, **kwargs)
self.files = files if files is not None else []

@property
def filtered_files(self) -> list[Path]:
return [
file
for file in self.files
if self.filter == "" or (self.filter and self.filter in file.name)
]

def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
return len(self.filtered_files)

def render(self, style: Style) -> RenderableType:
grid = Table.grid()
grid.add_column()
for file in self.filtered_files:
file_text = Text(f" {file.name}")
if self.filter:
file_text.highlight_regex(self.filter, "black on yellow")
grid.add_row(file_text)
return grid


class FileSearchApp(App):
dark = True

def on_mount(self) -> None:
self.file_table = FileTable(id="file_table", files=list(Path.cwd().iterdir()))
self.search_bar = TextInput(placeholder="Search for files...")
self.search_bar.focus()
self.mount(file_table_wrapper=Widget(self.file_table))
self.mount(search_bar=self.search_bar)

def handle_changed(self, event: TextWidgetBase.Changed) -> None:
self.file_table.filter = event.value


app = FileSearchApp(
log_path="textual.log", css_path="file_search.scss", watch_css=True, log_verbosity=2
)

if __name__ == "__main__":
result = app.run()
21 changes: 21 additions & 0 deletions sandbox/file_search.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Screen {
layout: dock;
docks: top=top bottom=bottom;
}

#file_table_wrapper {
dock: bottom;
height: auto;
overflow: auto auto;
scrollbar-color: $accent-darken-1;
}

#file_table {
height: auto;
}

#search_bar {
dock: bottom;
background: $accent;
height: 1;
}
28 changes: 26 additions & 2 deletions sandbox/input.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from __future__ import annotations

from pathlib import Path

from textual.app import App
from textual.widget import Widget

Expand All @@ -12,6 +16,18 @@ def fahrenheit_to_celsius(fahrenheit: float) -> float:
return (fahrenheit - 32) / 1.8


words = set(Path("/usr/share/dict/words").read_text().splitlines())


def word_autocompleter(value: str) -> str | None:
# An example autocompleter that uses the Unix dictionary to suggest
# word completions
for word in words:
if word.startswith(value):
return word
return None


class InputApp(App[str]):
def on_mount(self) -> None:
self.fahrenheit = TextInput(placeholder="Fahrenheit", id="fahrenheit")
Expand All @@ -20,7 +36,16 @@ def on_mount(self) -> None:
text_boxes = Widget(self.fahrenheit, self.celsius)
self.mount(inputs=text_boxes)
self.mount(spacer=Widget())
self.mount(footer=TextInput(placeholder="Footer Search Bar"))
self.mount(
top_search=Widget(
TextInput(autocompleter=word_autocompleter, id="topsearchbox")
)
)
self.mount(
footer=TextInput(
placeholder="Footer Search Bar", autocompleter=word_autocompleter
)
)
self.mount(text_area=TextArea())

def handle_changed(self, event: TextWidgetBase.Changed) -> None:
Expand All @@ -42,4 +67,3 @@ def handle_changed(self, event: TextWidgetBase.Changed) -> None:

if __name__ == "__main__":
result = app.run()
print(repr(result))
15 changes: 10 additions & 5 deletions sandbox/input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ App {
Screen {
layout: dock;
docks: top=top bottom=bottom;
background: $secondary;
background: $background;
}

#fahrenheit {
Expand All @@ -37,12 +37,17 @@ Screen {
dock: bottom;
}

#top_search {
dock: top;
}

#topsearchbox {
width: 10%;
}

#footer {
background: $primary-darken-2;
dock: bottom;
height: 1;
border: ;
}

#footer :focus {
border: heavy $secondary;
}
18 changes: 14 additions & 4 deletions src/textual/_text_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def cursor_right(self) -> bool:
return previous_index != new_index

def query_cursor_left(self) -> bool:
"""Check if the cursor can move 1 character left in the text.
"""Check if the cursor can move 1 codepoint left in the text.

Returns:
bool: True if the cursor can move left. False otherwise.
Expand All @@ -83,15 +83,21 @@ def query_cursor_left(self) -> bool:
new_index = max(0, previous_index - 1)
return previous_index != new_index

def query_cursor_right(self) -> bool:
def query_cursor_right(self) -> str | None:
"""Check if the cursor can move right (we can't move right if we're at the end)
and return the codepoint to the right of the cursor if it exists. If it doesn't
exist (e.g. we're at the end), then return None

Returns:
bool: True if the cursor can move right. False otherwise.
str: The codepoint to the right of the cursor if it exists, otherwise None.
"""
previous_index = self.cursor_index
new_index = min(len(self.content), previous_index + 1)
return previous_index != new_index
if new_index == len(self.content):
return None
elif previous_index != new_index:
return self.content[new_index]
return None

def cursor_text_start(self) -> bool:
"""Move the cursor to the start of the text
Expand Down Expand Up @@ -147,3 +153,7 @@ def get_range(self, start: int, end: int) -> str:
str: The sliced string between start and end.
"""
return self.content[start:end]

@property
def cursor_at_end(self):
return self.cursor_index == len(self.content)
Loading