From f029383f965b93aae50d364f3ef90ac19240ef0b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 4 Dec 2024 13:25:42 +0000 Subject: [PATCH 01/34] Initial machinery for text selection in Input widget --- src/textual/widgets/_input.py | 90 +++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 14 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index b25ac0af36..2e2e422d29 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -2,7 +2,7 @@ import re from dataclasses import dataclass -from typing import TYPE_CHECKING, ClassVar, Iterable +from typing import TYPE_CHECKING, ClassVar, Iterable, NamedTuple from rich.cells import cell_len, get_character_cell_size from rich.console import Console, ConsoleOptions, RenderableType @@ -21,7 +21,7 @@ from textual.binding import Binding, BindingType from textual.css._error_tools import friendly_list from textual.events import Blur, Focus, Mount -from textual.geometry import Offset, Size +from textual.geometry import Offset, Size, clamp from textual.message import Message from textual.reactive import Reactive, reactive, var from textual.suggester import Suggester, SuggestionReady @@ -89,6 +89,26 @@ def __rich_console__( yield from line +class Selection(NamedTuple): + """A range of selected text within the Input. + + Text can be selected by clicking and dragging the mouse, or by pressing + shift+arrow keys. + + Attributes: + start: The start index of the selection. + end: The end index of the selection. + """ + + start: int + end: int + + @classmethod + def cursor(cls, cursor_position: int) -> Selection: + """Create a selection from a cursor position.""" + return cls(cursor_position, cursor_position) + + class Input(Widget, can_focus=True): """A text input widget.""" @@ -197,7 +217,13 @@ class Input(Widget, can_focus=True): cursor_blink = reactive(True, init=False) value = reactive("", layout=True, init=False) input_scroll_offset = reactive(0) + + # TODO - remove and replace with selection. Change to property? cursor_position: Reactive[int] = reactive(0) + + selection: Reactive[Selection] = reactive(Selection.cursor(0)) + """The currently selected range of text.""" + view_position = reactive(0) placeholder = reactive("") complete = reactive("") @@ -358,6 +384,9 @@ def __init__( elif self.type == "number": self.validators.append(Number()) + self._selecting = False + """True if the user is selecting text with the mouse.""" + self._initial_value = True """Indicates if the value has been set for the first time yet.""" if value is not None: @@ -431,7 +460,7 @@ def _watch_cursor_blink(self, blink: bool) -> None: if blink: self._blink_timer.resume() else: - self._pause_blink_cycle() + self._pause_blink() self._cursor_visible = True @property @@ -559,17 +588,17 @@ def _on_mount(self, event: Mount) -> None: ) def _on_blur(self, event: Blur) -> None: - self._pause_blink_cycle() + self._pause_blink() if "blur" in self.validate_on: self.validate(self.value) def _on_focus(self, event: Focus) -> None: - self._restart_blink_cycle() + self._restart_blink() self.app.cursor_position = self.cursor_screen_offset self._suggestion = "" async def _on_key(self, event: events.Key) -> None: - self._restart_blink_cycle() + self._restart_blink() if event.is_printable: event.stop() @@ -584,9 +613,7 @@ def _on_paste(self, event: events.Paste) -> None: event.stop() async def _on_click(self, event: events.Click) -> None: - offset = event.get_content_offset(self) - if offset is None: - return + offset = event.get_content_offset_capture(self) event.stop() click_x = offset.x + self.view_position cell_offset = 0 @@ -600,26 +627,61 @@ async def _on_click(self, event: events.Click) -> None: else: self.cursor_position = len(self.value) + def _cell_offset_to_index(self, offset: int) -> int: + """Convert a cell offset to a character index, accounting for character width. + + Args: + offset: The cell offset to convert. + + Returns: + The character index corresponding to the cell offset. + """ + cell_offset = 0 + _cell_size = get_character_cell_size + offset += self.view_position + for index, char in enumerate(self.value): + cell_width = _cell_size(char) + if cell_offset <= offset < (cell_offset + cell_width): + return index + cell_offset += cell_width + return clamp(offset, 0, len(self.value)) + async def _on_mouse_down(self, event: events.MouseDown) -> None: - self._pause_blink_cycle() + self._pause_blink(visible=True) + offset_x, _ = event.get_content_offset_capture(self) + self.selection = Selection.cursor(self._cell_offset_to_index(offset_x)) + self._selecting = True + self.capture_mouse() async def _on_mouse_up(self, event: events.MouseUp) -> None: - self._restart_blink_cycle() + self._restart_blink() + self._selecting = False + self.release_mouse() + + async def _on_mouse_move(self, event: events.MouseMove) -> None: + offset = event.get_content_offset_capture(self) + if self._selecting: + # As we drag the mouse, we update the end position of the selection, + # keeping the start position fixed. + selection_start, _ = self.selection + self.selection = Selection( + selection_start, self._cell_offset_to_index(offset.x) + ) async def _on_suggestion_ready(self, event: SuggestionReady) -> None: """Handle suggestion messages and set the suggestion when relevant.""" if event.value == self.value: self._suggestion = event.suggestion - def _restart_blink_cycle(self) -> None: + def _restart_blink(self) -> None: """Restart the cursor blink cycle.""" self._cursor_visible = True if self.cursor_blink and self._blink_timer: self._blink_timer.reset() - def _pause_blink_cycle(self) -> None: + def _pause_blink(self, visible: bool = False) -> None: """Hide the blinking cursor and pause the blink cycle.""" - self._cursor_visible = False + self._cursor_visible = visible if self._blink_timer: self._blink_timer.pause() From 24cad3c308fcbb306e72e0ca2136b59430d5c59f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 4 Dec 2024 13:27:35 +0000 Subject: [PATCH 02/34] Demoting cursor_position to property --- src/textual/widgets/_input.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 2e2e422d29..b945105600 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -219,7 +219,14 @@ class Input(Widget, can_focus=True): input_scroll_offset = reactive(0) # TODO - remove and replace with selection. Change to property? - cursor_position: Reactive[int] = reactive(0) + # cursor_position: Reactive[int] = reactive(0) + @property + def cursor_position(self) -> int: + return self._cell_offset_to_index(self.selection.end) + + @cursor_position.setter + def cursor_position(self, position: int) -> None: + self.selection = Selection.cursor(position) selection: Reactive[Selection] = reactive(Selection.cursor(0)) """The currently selected range of text.""" From a1cefa584501906ec62abe0d1b0b7d61b414257a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 4 Dec 2024 13:31:56 +0000 Subject: [PATCH 03/34] Update validator and watcher to work with Input.selection instead of Input.cursor_position --- src/textual/widgets/_input.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index b945105600..0129d078e8 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -434,15 +434,18 @@ def check_consume_key(self, key: str, character: str | None) -> bool: """ return character is not None and character.isprintable() - def validate_cursor_position(self, cursor_position: int) -> int: - return min(max(0, cursor_position), len(self.value)) + def validate_selection(self, selection: Selection) -> Selection: + return Selection( + clamp(selection.start, 0, len(self.value)), + clamp(selection.end, 0, len(self.value)), + ) def validate_view_position(self, view_position: int) -> int: width = self.content_size.width new_view_position = max(0, min(view_position, self.cursor_width - width)) return new_view_position - def _watch_cursor_position(self) -> None: + def _watch_selection(self) -> None: width = self.content_size.width if width == 0: # If the input has no width the view position can't be elsewhere. From 9c8cfcb202d2c92e2c1c91a9d87e3f22405f2460 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 4 Dec 2024 13:57:46 +0000 Subject: [PATCH 04/34] Click and drag to select --- src/textual/widgets/_input.py | 72 ++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 0129d078e8..8a69338d12 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -45,9 +45,12 @@ class _InputRenderable: """Render the input content.""" - def __init__(self, input: Input, cursor_visible: bool) -> None: + def __init__( + self, input: Input, cursor_visible: bool, selection: Selection + ) -> None: self.input = input self.cursor_visible = cursor_visible + self.selection = selection def __rich_console__( self, console: "Console", options: "ConsoleOptions" @@ -67,12 +70,19 @@ def __rich_console__( input.get_component_rich_style("input--suggestion"), ) - if self.cursor_visible and input.has_focus: - if not show_suggestion and input._cursor_at_end: - result.pad_right(1) - cursor_style = input.get_component_rich_style("input--cursor") - cursor = input.cursor_position - result.stylize(cursor_style, cursor, cursor + 1) + if input.has_focus: + # TODO - use a component class + if not self.selection.empty: + start, end = sorted((self.selection.start, self.selection.end)) + selection_style = input.get_component_rich_style("input--selection") + result.stylize(selection_style, start, end) + + if self.cursor_visible: + if not show_suggestion and input._cursor_at_end: + result.pad_right(1) + cursor_style = input.get_component_rich_style("input--cursor") + cursor = input.cursor_position + result.stylize(cursor_style, cursor, cursor + 1) segments = list(result.render(console)) line_length = Segment.get_line_length(segments) @@ -108,6 +118,10 @@ def cursor(cls, cursor_position: int) -> Selection: """Create a selection from a cursor position.""" return cls(cursor_position, cursor_position) + @property + def empty(self) -> bool: + return self.start == self.end + class Input(Widget, can_focus=True): """A text input widget.""" @@ -155,6 +169,7 @@ class Input(Widget, can_focus=True): "input--cursor", "input--placeholder", "input--suggestion", + "input--selection", } """ | Class | Description | @@ -162,6 +177,7 @@ class Input(Widget, can_focus=True): | `input--cursor` | Target the cursor. | | `input--placeholder` | Target the placeholder text (when it exists). | | `input--suggestion` | Target the auto-completion suggestion (when it exists). | + | `input--selection` | Target the selected text. | """ DEFAULT_CSS = """ @@ -182,6 +198,9 @@ class Input(Widget, can_focus=True): color: $input-cursor-foreground; text-style: $input-cursor-text-style; } + &>.input--selection { + background: $input-selection-background; + } &>.input--placeholder, &>.input--suggestion { color: $text-disabled; } @@ -435,17 +454,14 @@ def check_consume_key(self, key: str, character: str | None) -> bool: return character is not None and character.isprintable() def validate_selection(self, selection: Selection) -> Selection: - return Selection( - clamp(selection.start, 0, len(self.value)), - clamp(selection.end, 0, len(self.value)), - ) + start, end = selection + value_length = len(self.value) + return Selection(clamp(start, 0, value_length), clamp(end, 0, value_length)) def validate_view_position(self, view_position: int) -> int: - width = self.content_size.width - new_view_position = max(0, min(view_position, self.cursor_width - width)) - return new_view_position + return clamp(view_position, 0, self.cursor_width - self.content_size.width) - def _watch_selection(self) -> None: + def _watch_selection(self, selection: Selection) -> None: width = self.content_size.width if width == 0: # If the input has no width the view position can't be elsewhere. @@ -567,7 +583,7 @@ def render(self) -> RenderResult: placeholder = Text(" ") placeholder.stylize(cursor_style, 0, 1) return placeholder - return _InputRenderable(self, self._cursor_visible) + return _InputRenderable(self, self._cursor_visible, self.selection) @property def _value(self) -> Text: @@ -622,21 +638,6 @@ def _on_paste(self, event: events.Paste) -> None: self.insert_text_at_cursor(line) event.stop() - async def _on_click(self, event: events.Click) -> None: - offset = event.get_content_offset_capture(self) - 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): - cell_width = _cell_size(char) - if cell_offset <= click_x < (cell_offset + cell_width): - self.cursor_position = index - break - cell_offset += cell_width - else: - self.cursor_position = len(self.value) - def _cell_offset_to_index(self, offset: int) -> int: """Convert a cell offset to a character index, accounting for character width. @@ -664,15 +665,16 @@ async def _on_mouse_down(self, event: events.MouseDown) -> None: self.capture_mouse() async def _on_mouse_up(self, event: events.MouseUp) -> None: - self._restart_blink() - self._selecting = False - self.release_mouse() + if self._selecting: + self._selecting = False + self.release_mouse() + self._restart_blink() async def _on_mouse_move(self, event: events.MouseMove) -> None: - offset = event.get_content_offset_capture(self) if self._selecting: # As we drag the mouse, we update the end position of the selection, # keeping the start position fixed. + offset = event.get_content_offset_capture(self) selection_start, _ = self.selection self.selection = Selection( selection_start, self._cell_offset_to_index(offset.x) From e376a4eed7e95aebfae3f0e254bd48ff8be8f077 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 4 Dec 2024 13:59:04 +0000 Subject: [PATCH 05/34] Remove input selection foreground --- docs/guide/design.md | 1 - src/textual/design.py | 3 --- src/textual/theme.py | 1 - 3 files changed, 5 deletions(-) diff --git a/docs/guide/design.md b/docs/guide/design.md index 4226d06aaf..08fc2bb42b 100644 --- a/docs/guide/design.md +++ b/docs/guide/design.md @@ -276,7 +276,6 @@ Here's a comprehensive list of these variables, their purposes, and default valu | `$input-cursor-foreground` | Text color of the input cursor | `$background` | | `$input-cursor-text-style` | Text style of the input cursor | `"none"` | | `$input-selection-background` | Background color of selected text | `$primary-lighten-1` with 40% opacity | -| `$input-selection-foreground` | Text color of selected text | `$background` | ### Scrollbar diff --git a/src/textual/design.py b/src/textual/design.py index f782b8803d..33d0e6c3e4 100644 --- a/src/textual/design.py +++ b/src/textual/design.py @@ -321,9 +321,6 @@ def luminosity_range(spread: float) -> Iterable[tuple[str, float]]: "input-selection-background", Color.parse(colors["primary-lighten-1"]).with_alpha(0.4).hex, ) - colors["input-selection-foreground"] = get( - "input-selection-foreground", background.hex - ) # Markdown header styles colors["markdown-h1-color"] = get("markdown-h1-color", primary.hex) diff --git a/src/textual/theme.py b/src/textual/theme.py index f3ff5045df..af36369b8f 100644 --- a/src/textual/theme.py +++ b/src/textual/theme.py @@ -171,7 +171,6 @@ def to_color_system(self) -> ColorSystem: "block-cursor-text-style": "b", "block-cursor-blurred-text-style": "i", "input-selection-background": "ansi_blue", - "input-selection-foreground": "ansi_white", "input-cursor-text-style": "reverse", "scrollbar": "ansi_blue", "border-blurred": "ansi_blue", From f01bcc88db23b0e4d35e45d35d8643020c771e95 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 4 Dec 2024 14:30:19 +0000 Subject: [PATCH 06/34] Select left and right words --- src/textual/widgets/_input.py | 125 +++++++++++++++++++++++++++------- 1 file changed, 100 insertions(+), 25 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 8a69338d12..f1675cdfa0 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -128,11 +128,35 @@ class Input(Widget, can_focus=True): BINDINGS: ClassVar[list[BindingType]] = [ Binding("left", "cursor_left", "Move cursor left", show=False), + Binding( + "shift+left", + "cursor_left(True)", + "Move cursor left and select", + show=False, + ), Binding("ctrl+left", "cursor_left_word", "Move cursor left a word", show=False), + Binding( + "ctrl+shift+left", + "cursor_left_word(True)", + "Move cursor left a word and select", + show=False, + ), Binding("right", "cursor_right", "Move cursor right", show=False), + Binding( + "shift+right", + "cursor_right(True)", + "Move cursor right and select", + show=False, + ), Binding( "ctrl+right", "cursor_right_word", "Move cursor right a word", show=False ), + Binding( + "ctrl+shift+right", + "cursor_right_word(True)", + "Move cursor right a word and select", + show=False, + ), Binding("backspace", "delete_left", "Delete character left", show=False), Binding("home,ctrl+a", "home", "Go to start", show=False), Binding("end,ctrl+e", "end", "Go to end", show=False), @@ -753,56 +777,107 @@ def clear(self) -> None: """Clear the input.""" self.value = "" - def action_cursor_left(self) -> None: - """Move the cursor one position to the left.""" - self.cursor_position -= 1 + def action_cursor_left(self, select: bool = False) -> None: + """Move the cursor one position to the left. - def action_cursor_right(self) -> None: - """Accept an auto-completion or move the cursor one position to the right.""" - if self._cursor_at_end and self._suggestion: - self.value = self._suggestion - self.cursor_position = len(self.value) + Args: + select: If `True`, select the text to the left of the cursor. + """ + if select: + start, end = self.selection + self.selection = Selection(start, end - 1) + else: + self.cursor_position -= 1 + + def action_cursor_right(self, select: bool = False) -> None: + """Accept an auto-completion or move the cursor one position to the right. + + Args: + select: If `True`, select the text to the right of the cursor. + """ + if select: + start, end = self.selection + self.selection = Selection(start, end + 1) + else: + if self._cursor_at_end and self._suggestion: + self.value = self._suggestion + self.cursor_position = len(self.value) + else: + self.cursor_position += 1 + + def action_home(self, select: bool = False) -> None: + """Move the cursor to the start of the input. + + Args: + select: If `True`, select the text between the old and new cursor positions. + """ + if select: + self.selection = Selection(0, self.cursor_position) else: - self.cursor_position += 1 + self.cursor_position = 0 - def action_home(self) -> None: - """Move the cursor to the start of the input.""" - self.cursor_position = 0 + def action_end(self, select: bool = False) -> None: + """Move the cursor to the end of the input. - def action_end(self) -> None: - """Move the cursor to the end of the input.""" - self.cursor_position = len(self.value) + Args: + select: If `True`, select the text between the old and new cursor positions. + """ + if select: + self.selection = Selection(self.cursor_position, len(self.value)) + else: + self.cursor_position = len(self.value) _WORD_START = re.compile(r"(?<=\W)\w") - def action_cursor_left_word(self) -> None: - """Move the cursor left to the start of a word.""" + def action_cursor_left_word(self, select: bool = False) -> None: + """Move the cursor left to the start of a word. + + Args: + select: If `True`, select the text between the old and new cursor positions. + """ if self.password: # This is a password field so don't give any hints about word # boundaries, even during movement. - self.action_home() + self.action_home(select) else: + start, _ = self.selection try: *_, hit = re.finditer( self._WORD_START, self.value[: self.cursor_position] ) except ValueError: - self.cursor_position = 0 + target = 0 else: - self.cursor_position = hit.start() + target = hit.start() + + if select: + self.selection = Selection(start, target) + else: + self.cursor_position = target - def action_cursor_right_word(self) -> None: - """Move the cursor right to the start of a word.""" + def action_cursor_right_word(self, select: bool = False) -> None: + """Move the cursor right to the start of a word. + + Args: + select: If `True`, select the text between the old and new cursor positions. + """ if self.password: # This is a password field so don't give any hints about word # boundaries, even during movement. - self.action_end() + self.action_end(select) else: hit = re.search(self._WORD_START, self.value[self.cursor_position :]) + + start, end = self.selection if hit is None: - self.cursor_position = len(self.value) + target = len(self.value) + else: + target = end + hit.start() + + if select: + self.selection = Selection(start, target) else: - self.cursor_position += hit.start() + self.cursor_position = target def action_delete_right(self) -> None: """Delete one character at the current cursor position.""" From e1bf6ff876b1e0d8cd357ce0430289cc17f9991a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 4 Dec 2024 15:49:36 +0000 Subject: [PATCH 07/34] Use line API --- src/textual/widgets/_input.py | 175 ++++++++++++++++------------------ 1 file changed, 80 insertions(+), 95 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index f1675cdfa0..9dc71ea6e7 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -5,29 +5,28 @@ from typing import TYPE_CHECKING, ClassVar, Iterable, NamedTuple from rich.cells import cell_len, get_character_cell_size -from rich.console import Console, ConsoleOptions, RenderableType -from rich.console import RenderResult as RichRenderResult +from rich.console import RenderableType from rich.highlighter import Highlighter -from rich.segment import Segment from rich.text import Text from typing_extensions import Literal from textual import events -from textual._segment_tools import line_crop +from textual.expand_tabs import expand_tabs_inline +from textual.scroll_view import ScrollView +from textual.strip import Strip if TYPE_CHECKING: - from textual.app import RenderResult + pass from textual.binding import Binding, BindingType from textual.css._error_tools import friendly_list from textual.events import Blur, Focus, Mount -from textual.geometry import Offset, Size, clamp +from textual.geometry import Offset, Region, Size, clamp from textual.message import Message from textual.reactive import Reactive, reactive, var from textual.suggester import Suggester, SuggestionReady from textual.timer import Timer from textual.validation import ValidationResult, Validator -from textual.widget import Widget InputValidationOn = Literal["blur", "changed", "submitted"] """Possible messages that trigger input validation.""" @@ -42,63 +41,6 @@ InputType = Literal["integer", "number", "text"] -class _InputRenderable: - """Render the input content.""" - - def __init__( - self, input: Input, cursor_visible: bool, selection: Selection - ) -> None: - self.input = input - self.cursor_visible = cursor_visible - self.selection = selection - - def __rich_console__( - self, console: "Console", options: "ConsoleOptions" - ) -> RichRenderResult: - input = self.input - result = input._value - width = input.content_size.width - - # Add the completion with a faded style. - value = input.value - value_length = len(value) - suggestion = input._suggestion - show_suggestion = len(suggestion) > value_length and input.has_focus - if show_suggestion: - result += Text( - suggestion[value_length:], - input.get_component_rich_style("input--suggestion"), - ) - - if input.has_focus: - # TODO - use a component class - if not self.selection.empty: - start, end = sorted((self.selection.start, self.selection.end)) - selection_style = input.get_component_rich_style("input--selection") - result.stylize(selection_style, start, end) - - if self.cursor_visible: - if not show_suggestion and input._cursor_at_end: - result.pad_right(1) - cursor_style = input.get_component_rich_style("input--cursor") - cursor = input.cursor_position - result.stylize(cursor_style, cursor, cursor + 1) - - 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 Selection(NamedTuple): """A range of selected text within the Input. @@ -123,7 +65,7 @@ def empty(self) -> bool: return self.start == self.end -class Input(Widget, can_focus=True): +class Input(ScrollView): """A text input widget.""" BINDINGS: ClassVar[list[BindingType]] = [ @@ -212,6 +154,7 @@ class Input(Widget, can_focus=True): border: tall $border-blurred; width: 100%; height: 3; + scrollbar-size-horizontal: 0; &:focus { border: tall $border; @@ -259,10 +202,7 @@ class Input(Widget, can_focus=True): cursor_blink = reactive(True, init=False) value = reactive("", layout=True, init=False) - input_scroll_offset = reactive(0) - # TODO - remove and replace with selection. Change to property? - # cursor_position: Reactive[int] = reactive(0) @property def cursor_position(self) -> int: return self._cell_offset_to_index(self.selection.end) @@ -274,7 +214,6 @@ def cursor_position(self, position: int) -> None: selection: Reactive[Selection] = reactive(Selection.cursor(0)) """The currently selected range of text.""" - view_position = reactive(0) placeholder = reactive("") complete = reactive("") width = reactive(1) @@ -447,8 +386,7 @@ def __init__( 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 + return cell_len(expand_tabs_inline(self.value[:position], 4)) @property def _cursor_offset(self) -> int: @@ -482,27 +420,18 @@ def validate_selection(self, selection: Selection) -> Selection: value_length = len(self.value) return Selection(clamp(start, 0, value_length), clamp(end, 0, value_length)) - def validate_view_position(self, view_position: int) -> int: - return clamp(view_position, 0, self.cursor_width - self.content_size.width) - def _watch_selection(self, selection: Selection) -> None: width = self.content_size.width if width == 0: # If the input has no width the view position can't be elsewhere. - self.view_position = 0 return - view_start = self.view_position - view_end = view_start + width - cursor_offset = self._cursor_offset - - 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 - self.app.cursor_position = self.cursor_screen_offset + self.scroll_to_region( + Region(self.cursor_position, 0, width=3, height=1), + force=True, + animate=False, + ) def _watch_cursor_blink(self, blink: bool) -> None: """Ensure we handle updating the cursor blink at runtime.""" @@ -517,7 +446,8 @@ def _watch_cursor_blink(self, blink: bool) -> None: def cursor_screen_offset(self) -> Offset: """The offset of the cursor of this input in screen-space. (x, y)/(column, row)""" x, y, _width, _height = self.content_region - return Offset(x + self._cursor_offset - self.view_position, y) + scroll_x, _ = self.scroll_offset + return Offset(x + self._cursor_offset - scroll_x, y) def _watch_value(self, value: str) -> None: self._suggestion = "" @@ -536,6 +466,8 @@ def _watch_value(self, value: str) -> None: self.cursor_position = len(self.value) self._initial_value = False + self.virtual_size = Size(self.cursor_width, 1) + def _watch_valid_empty(self) -> None: """Repeat validation when valid_empty changes.""" self._watch_value(self.value) @@ -593,8 +525,9 @@ def cursor_width(self) -> int: return cell_len(self.placeholder) return self._position_to_cell(len(self.value)) + 1 - def render(self) -> RenderResult: - self.view_position = self.view_position + def render_line(self, y: int) -> Strip: + console = self.app.console + width = self.scrollable_content_region.size.width if not self.value: placeholder = Text(self.placeholder, justify="left") placeholder.stylize(self.get_component_rich_style("input--placeholder")) @@ -606,8 +539,56 @@ def render(self) -> RenderResult: if len(placeholder) == 0: placeholder = Text(" ") placeholder.stylize(cursor_style, 0, 1) - return placeholder - return _InputRenderable(self, self._cursor_visible, self.selection) + strip = Strip( + console.render(placeholder, console.options.update_width(width)) + ) + else: + result = self._value + + # Add the completion with a faded style. + value = self.value + value_length = len(value) + suggestion = self._suggestion + show_suggestion = len(suggestion) > value_length and self.has_focus + if show_suggestion: + result += Text( + suggestion[value_length:], + self.get_component_rich_style("input--suggestion"), + ) + + if self.has_focus: + if not self.selection.empty: + start, end = self.selection + start, end = sorted((start, end)) + selection_style = self.get_component_rich_style("input--selection") + result.stylize(selection_style, start, end) + + if self._cursor_visible: + if not show_suggestion and self._cursor_at_end: + result.pad_right(1) + cursor_style = self.get_component_rich_style("input--cursor") + cursor = self.cursor_position + result.stylize(cursor_style, cursor, cursor + 1) + + segments = list( + console.render( + result, + console.options.update_width( + max(self.virtual_size.width, self.region.size.width) + ), + ) + ) + + strip = Strip(segments) + virtual_width = self.virtual_size.width + scroll_x, _ = self.scroll_offset + strip = strip.crop( + scroll_x, scroll_x + self.scrollable_content_region.size.width + ) + strip = strip.adjust_cell_length(virtual_width) + strip = strip.simplify() + + return strip.apply_style(self.rich_style) @property def _value(self) -> Text: @@ -673,7 +654,8 @@ def _cell_offset_to_index(self, offset: int) -> int: """ cell_offset = 0 _cell_size = get_character_cell_size - offset += self.view_position + scroll_x, _ = self.scroll_offset + offset += scroll_x for index, char in enumerate(self.value): cell_width = _cell_size(char) if cell_offset <= offset < (cell_offset + cell_width): @@ -681,10 +663,15 @@ def _cell_offset_to_index(self, offset: int) -> int: cell_offset += cell_width return clamp(offset, 0, len(self.value)) + def _offset_to_index(self, offset: int) -> int: + """Convert an offset to a character index, accounting for view position.""" + scroll_x, _ = self.scroll_offset + return self._cell_offset_to_index(offset - scroll_x) + async def _on_mouse_down(self, event: events.MouseDown) -> None: self._pause_blink(visible=True) offset_x, _ = event.get_content_offset_capture(self) - self.selection = Selection.cursor(self._cell_offset_to_index(offset_x)) + self.selection = Selection.cursor(self._offset_to_index(offset_x)) self._selecting = True self.capture_mouse() @@ -700,9 +687,7 @@ async def _on_mouse_move(self, event: events.MouseMove) -> None: # keeping the start position fixed. offset = event.get_content_offset_capture(self) selection_start, _ = self.selection - self.selection = Selection( - selection_start, self._cell_offset_to_index(offset.x) - ) + self.selection = Selection(selection_start, self._offset_to_index(offset.x)) async def _on_suggestion_ready(self, event: SuggestionReady) -> None: """Handle suggestion messages and set the suggestion when relevant.""" From bd39e0f7d798c4b0684dbbfebb0944565b3802c3 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 4 Dec 2024 15:52:21 +0000 Subject: [PATCH 08/34] Remove more unused reactives --- src/textual/widgets/_input.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 9dc71ea6e7..e33fe0149b 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -215,8 +215,6 @@ def cursor_position(self, position: int) -> None: """The currently selected range of text.""" placeholder = reactive("") - complete = reactive("") - width = reactive(1) _cursor_visible = reactive(True) password = reactive(False) suggester: Suggester | None @@ -421,11 +419,6 @@ def validate_selection(self, selection: Selection) -> Selection: return Selection(clamp(start, 0, value_length), clamp(end, 0, value_length)) def _watch_selection(self, selection: Selection) -> None: - width = self.content_size.width - if width == 0: - # If the input has no width the view position can't be elsewhere. - return - self.app.cursor_position = self.cursor_screen_offset self.scroll_to_region( Region(self.cursor_position, 0, width=3, height=1), From 0e0ca7c4f6224bfdbf65aa82c694d94130c682c6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 4 Dec 2024 18:06:48 +0000 Subject: [PATCH 09/34] Fixes --- src/textual/widgets/_input.py | 91 ++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index e33fe0149b..fe1eb1f203 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -201,11 +201,12 @@ class Input(ScrollView): """ cursor_blink = reactive(True, init=False) - value = reactive("", layout=True, init=False) + # TODO - check with width: auto to see if layout=True is needed + value = reactive("", init=False) @property def cursor_position(self) -> int: - return self._cell_offset_to_index(self.selection.end) + return self.selection.end @cursor_position.setter def cursor_position(self, position: int) -> None: @@ -383,7 +384,14 @@ def __init__( self.tooltip = tooltip def _position_to_cell(self, position: int) -> int: - """Convert an index within the value to cell position.""" + """Convert an index within the value to cell position. + + Args: + position: The index within the value to convert. + + Returns: + The cell position corresponding to the index. + """ return cell_len(expand_tabs_inline(self.value[:position], 4)) @property @@ -397,7 +405,7 @@ def _cursor_offset(self) -> int: @property def _cursor_at_end(self) -> bool: """Flag to indicate if the cursor is at the end""" - return self.cursor_position >= len(self.value) + return self.cursor_position == len(self.value) def check_consume_key(self, key: str, character: str | None) -> bool: """Check if the widget may consume the given key. @@ -421,7 +429,7 @@ def validate_selection(self, selection: Selection) -> Selection: def _watch_selection(self, selection: Selection) -> None: self.app.cursor_position = self.cursor_screen_offset self.scroll_to_region( - Region(self.cursor_position, 0, width=3, height=1), + Region(self.cursor_position, 0, width=1, height=1), force=True, animate=False, ) @@ -443,6 +451,8 @@ def cursor_screen_offset(self) -> Offset: return Offset(x + self._cursor_offset - scroll_x, y) def _watch_value(self, value: str) -> None: + """Update the virtual size and suggestion when the value changes.""" + self.virtual_size = Size(self.content_width, 1) self._suggestion = "" if self.suggester and value: self.run_worker(self.suggester._get_suggestion(self, value)) @@ -459,8 +469,6 @@ def _watch_value(self, value: str) -> None: self.cursor_position = len(self.value) self._initial_value = False - self.virtual_size = Size(self.cursor_width, 1) - def _watch_valid_empty(self) -> None: """Repeat validation when valid_empty changes.""" self._watch_value(self.value) @@ -511,18 +519,12 @@ def is_valid(self) -> bool: """Check if the value has passed validation.""" return self._valid - @property - def cursor_width(self) -> int: - """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 - def render_line(self, y: int) -> Strip: console = self.app.console - width = self.scrollable_content_region.size.width + max_content_width = self.scrollable_content_region.width + if not self.value: - placeholder = Text(self.placeholder, justify="left") + placeholder = Text(self.placeholder, justify="left", end="") placeholder.stylize(self.get_component_rich_style("input--placeholder")) if self.has_focus: cursor_style = self.get_component_rich_style("input--cursor") @@ -530,10 +532,13 @@ def render_line(self, y: int) -> Strip: # If the placeholder is empty, there's no characters to stylise # to make the cursor flash, so use a single space character if len(placeholder) == 0: - placeholder = Text(" ") + placeholder = Text(" ", end="") placeholder.stylize(cursor_style, 0, 1) + strip = Strip( - console.render(placeholder, console.options.update_width(width)) + console.render( + placeholder, console.options.update_width(max_content_width + 1) + ) ) else: result = self._value @@ -547,6 +552,7 @@ def render_line(self, y: int) -> Strip: result += Text( suggestion[value_length:], self.get_component_rich_style("input--suggestion"), + end="", ) if self.has_focus: @@ -557,29 +563,20 @@ def render_line(self, y: int) -> Strip: result.stylize(selection_style, start, end) if self._cursor_visible: - if not show_suggestion and self._cursor_at_end: - result.pad_right(1) cursor_style = self.get_component_rich_style("input--cursor") cursor = self.cursor_position + if not show_suggestion and self._cursor_at_end: + result.pad_right(1) result.stylize(cursor_style, cursor, cursor + 1) segments = list( - console.render( - result, - console.options.update_width( - max(self.virtual_size.width, self.region.size.width) - ), - ) + console.render(result, console.options.update_width(self.content_width)) ) strip = Strip(segments) - virtual_width = self.virtual_size.width scroll_x, _ = self.scroll_offset - strip = strip.crop( - scroll_x, scroll_x + self.scrollable_content_region.size.width - ) - strip = strip.adjust_cell_length(virtual_width) - strip = strip.simplify() + strip = strip.crop(scroll_x, scroll_x + max_content_width + 1) + strip = strip.extend_cell_length(max_content_width + 1) return strip.apply_style(self.rich_style) @@ -587,15 +584,25 @@ def render_line(self, y: int) -> Strip: def _value(self) -> Text: """Value rendered as text.""" if self.password: - return Text("•" * len(self.value), no_wrap=True, overflow="ignore") + return Text("•" * len(self.value), no_wrap=True, overflow="ignore", end="") else: - text = Text(self.value, no_wrap=True, overflow="ignore") + text = Text(self.value, no_wrap=True, overflow="ignore", end="") if self.highlighter is not None: text = self.highlighter(text) return text + @property + def content_width(self) -> int: + """The width of the content.""" + if self.placeholder and not self.value: + return cell_len(self.placeholder) + + # Extra space for cursor at the end. + return self._value.cell_len + 1 + def get_content_width(self, container: Size, viewport: Size) -> int: - return self.cursor_width + """Get the widget of the content.""" + return self.content_width def get_content_height(self, container: Size, viewport: Size, width: int) -> int: return 1 @@ -656,15 +663,10 @@ def _cell_offset_to_index(self, offset: int) -> int: cell_offset += cell_width return clamp(offset, 0, len(self.value)) - def _offset_to_index(self, offset: int) -> int: - """Convert an offset to a character index, accounting for view position.""" - scroll_x, _ = self.scroll_offset - return self._cell_offset_to_index(offset - scroll_x) - async def _on_mouse_down(self, event: events.MouseDown) -> None: self._pause_blink(visible=True) offset_x, _ = event.get_content_offset_capture(self) - self.selection = Selection.cursor(self._offset_to_index(offset_x)) + self.selection = Selection.cursor(self._cell_offset_to_index(offset_x)) self._selecting = True self.capture_mouse() @@ -680,7 +682,9 @@ async def _on_mouse_move(self, event: events.MouseMove) -> None: # keeping the start position fixed. offset = event.get_content_offset_capture(self) selection_start, _ = self.selection - self.selection = Selection(selection_start, self._offset_to_index(offset.x)) + self.selection = Selection( + selection_start, self._cell_offset_to_index(offset.x) + ) async def _on_suggestion_ready(self, event: SuggestionReady) -> None: """Handle suggestion messages and set the suggestion when relevant.""" @@ -891,7 +895,8 @@ def action_delete_left(self) -> None: if self.cursor_position <= 0: # Cursor at the start, so nothing to delete return - if self.cursor_position == len(self.value): + + if self._cursor_at_end: # Delete from end self.value = self.value[:-1] self.cursor_position = len(self.value) From 02c67bf072fd1d5b18a95e13c8b950d4e6a94820 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 10:46:46 +0000 Subject: [PATCH 10/34] Fix accidental multiple cursors --- src/textual/widgets/_input.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index fe1eb1f203..fbd9d7a561 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -520,6 +520,9 @@ def is_valid(self) -> bool: return self._valid def render_line(self, y: int) -> Strip: + if y != 0: + return Strip.blank(self.size.width) + console = self.app.console max_content_width = self.scrollable_content_region.width From 64adf927d095537002dc2567f0fc189c7854db46 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 11:06:58 +0000 Subject: [PATCH 11/34] Fix MaskedInput --- src/textual/widgets/_masked_input.py | 97 ++++++++++++---------------- 1 file changed, 41 insertions(+), 56 deletions(-) diff --git a/src/textual/widgets/_masked_input.py b/src/textual/widgets/_masked_input.py index 4fc0e00b3d..1649fb7640 100644 --- a/src/textual/widgets/_masked_input.py +++ b/src/textual/widgets/_masked_input.py @@ -5,17 +5,16 @@ from enum import Flag, auto from typing import TYPE_CHECKING, Iterable, Pattern -from rich.console import Console, ConsoleOptions, RenderableType -from rich.console import RenderResult as RichRenderResult +from rich.console import RenderableType from rich.segment import Segment from rich.text import Text from typing_extensions import Literal from textual import events -from textual._segment_tools import line_crop +from textual.strip import Strip if TYPE_CHECKING: - from textual.app import RenderResult + pass from textual.reactive import var from textual.validation import ValidationResult, Validator @@ -63,55 +62,6 @@ class _CharFlags(Flag): } -class _InputRenderable: - """Render the input content.""" - - def __init__(self, input: Input, cursor_visible: bool) -> None: - self.input = input - self.cursor_visible = cursor_visible - - def __rich_console__( - self, console: "Console", options: "ConsoleOptions" - ) -> RichRenderResult: - input = self.input - result = input._value - width = input.content_size.width - - # Add the completion with a faded style. - value = input.value - value_length = len(value) - template = input._template - style = input.get_component_rich_style("input--placeholder") - result += Text( - template.mask[value_length:], - style, - ) - for index, (char, char_definition) in enumerate(zip(value, template.template)): - if char == " ": - result.stylize(style, index, index + 1) - - if self.cursor_visible and input.has_focus: - if input._cursor_at_end: - result.pad_right(1) - cursor_style = input.get_component_rich_style("input--cursor") - cursor = input.cursor_position - result.stylize(cursor_style, cursor, cursor + 1) - - 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 _Template(Validator): """Template mask enforcer.""" @@ -596,14 +546,49 @@ def set_classes() -> None: return combined_result - def render(self) -> RenderResult: - return _InputRenderable(self, self._cursor_visible) + def render_line(self, y: int) -> Strip: + if y != 0: + return Strip.blank(self.size.width) + + result = self._value + width = self.content_size.width + + # Add the completion with a faded style. + value = self.value + value_length = len(value) + template = self._template + style = self.get_component_rich_style("input--placeholder") + result += Text( + template.mask[value_length:], + style, + end="", + ) + for index, (char, char_definition) in enumerate(zip(value, template.template)): + if char == " ": + result.stylize(style, index, index + 1) + + if self._cursor_visible and self.has_focus: + if self._cursor_at_end: + result.pad_right(1) + cursor_style = self.get_component_rich_style("input--cursor") + cursor = self.cursor_position + result.stylize(cursor_style, cursor, cursor + 1) + + segments = list(result.render(self.app.console)) + line_length = Segment.get_line_length(segments) + if line_length < width: + segments = Segment.adjust_line_length(segments, width) + line_length = width + + strip = Strip(segments) + strip = strip.crop(self.scroll_offset.x, self.scroll_offset.x + width) + return strip.apply_style(self.rich_style) @property def _value(self) -> Text: """Value rendered as text.""" value = self._template.display(self.value) - return Text(value, no_wrap=True, overflow="ignore") + return Text(value, no_wrap=True, overflow="ignore", end="") async def _on_click(self, event: events.Click) -> None: """Ensure clicking on value does not leave cursor on a separator.""" From 5c456566fa416a475e36c8c073901a7d344b7af6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 11:16:35 +0000 Subject: [PATCH 12/34] Fix scrolling with double width characters --- src/textual/widgets/_input.py | 2 +- src/textual/widgets/_masked_input.py | 3 +-- tests/input/test_input_mouse.py | 7 ++++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index fbd9d7a561..6b53c7f6c1 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -429,7 +429,7 @@ def validate_selection(self, selection: Selection) -> Selection: def _watch_selection(self, selection: Selection) -> None: self.app.cursor_position = self.cursor_screen_offset self.scroll_to_region( - Region(self.cursor_position, 0, width=1, height=1), + Region(self._cursor_offset, 0, width=1, height=1), force=True, animate=False, ) diff --git a/src/textual/widgets/_masked_input.py b/src/textual/widgets/_masked_input.py index 1649fb7640..ed6aef7527 100644 --- a/src/textual/widgets/_masked_input.py +++ b/src/textual/widgets/_masked_input.py @@ -580,8 +580,7 @@ def render_line(self, y: int) -> Strip: segments = Segment.adjust_line_length(segments, width) line_length = width - strip = Strip(segments) - strip = strip.crop(self.scroll_offset.x, self.scroll_offset.x + width) + strip = Strip(segments).crop(self.scroll_offset.x, self.scroll_offset.x + width) return strip.apply_style(self.rich_style) @property diff --git a/tests/input/test_input_mouse.py b/tests/input/test_input_mouse.py index a5249e5498..9793e21eac 100644 --- a/tests/input/test_input_mouse.py +++ b/tests/input/test_input_mouse.py @@ -68,11 +68,12 @@ async def test_mouse_clicks_within(text, click_at, should_land): assert pilot.app.query_one(Input).cursor_position == should_land -async def test_mouse_click_outwith(): - """Mouse clicks outside the input should not affect cursor position.""" +async def test_mouse_click_outwith_moves_cursor_to_nearest_cell(): + """Mouse clicks in the padding or border area should move the cursor as this makes + dragging and selecting text easier.""" async with InputApp(TEXT_SINGLE).run_test() as pilot: pilot.app.query_one(Input).cursor_position = 3 assert pilot.app.query_one(Input).cursor_position == 3 await pilot.click(Input, Offset(0, 0)) await pilot.pause() - assert pilot.app.query_one(Input).cursor_position == 3 + assert pilot.app.query_one(Input).cursor_position == 0 From f54062222ae354780754a9cae2286b553e695d41 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 11:33:53 +0000 Subject: [PATCH 13/34] Update snapshots, remove old tests --- tests/input/test_input_value_visibility.py | 102 ------------ .../test_ansi_command_palette.svg | 132 +++++++-------- .../test_snapshots/test_app_blur.svg | 112 ++++++------- ...ommands_opens_and_displays_search_list.svg | 122 +++++++------- .../test_snapshots/test_auto_grid.svg | 116 +++++++------- .../test_snapshots/test_auto_width_input.svg | 118 +++++++------- .../test_snapshots/test_blur_on_disabled.svg | 114 ++++++------- .../test_check_consume_keys.svg | 126 +++++++-------- .../test_snapshots/test_command_palette.svg | 122 +++++++------- .../test_command_palette_discovery.svg | 122 +++++++------- .../test_example_dictionary.svg | 116 +++++++------- .../test_snapshots/test_help_panel.svg | 146 ++++++++--------- .../test_snapshots/test_input_and_focus.svg | 114 ++++++------- .../test_input_percentage_width.svg | 118 +++++++------- .../test_snapshots/test_input_suggestions.svg | 118 +++++++------- .../test_snapshots/test_input_validation.svg | 120 +++++++------- .../test_snapshots/test_layout_containers.svg | 132 +++++++-------- .../test_markup_command_list.svg | 128 +++++++-------- .../test_snapshots/test_masked_input.svg | 118 +++++++------- .../test_modal_dialog_bindings_input.svg | 126 +++++++-------- .../test_snapshots/test_system_commands.svg | 150 +++++++++--------- .../test_textual_dev_easing_preview.svg | 138 ++++++++-------- 22 files changed, 1304 insertions(+), 1406 deletions(-) delete mode 100644 tests/input/test_input_value_visibility.py diff --git a/tests/input/test_input_value_visibility.py b/tests/input/test_input_value_visibility.py deleted file mode 100644 index e2de635028..0000000000 --- a/tests/input/test_input_value_visibility.py +++ /dev/null @@ -1,102 +0,0 @@ -from rich.console import Console - -from textual.app import App -from textual.widgets import Input - - -async def test_input_value_visible_on_instantiation(): - """Check if the full input value is rendered if the input is instantiated with it.""" - - class MyApp(App): - def compose(self): - yield Input(value="value") - - app = MyApp() - async with app.run_test(): - console = Console(width=5) - with console.capture() as capture: - console.print(app.query_one(Input).render()) - assert capture.get() == "value" - - -async def test_input_value_visible_after_value_assignment(): - """Check if the full input value is rendered if the value is assigned to programmatically.""" - - class MyApp(App): - def compose(self): - yield Input() - - def on_mount(self): - self.query_one(Input).value = "value" - - app = MyApp() - async with app.run_test(): - console = Console(width=5) - with console.capture() as capture: - console.print(app.query_one(Input).render()) - assert capture.get() == "value" - - -async def test_input_value_visible_if_mounted_later(): - """Check if full input value is rendered if the widget is mounted later.""" - - class MyApp(App): - BINDINGS = [("a", "add_input", "add_input")] - - async def action_add_input(self): - await self.mount(Input(value="value")) - - app = MyApp() - async with app.run_test() as pilot: - await pilot.press("a") - await pilot.pause() - console = Console(width=5) - with console.capture() as capture: - console.print(app.query_one(Input).render()) - assert capture.get() == "value" - - -async def test_input_value_visible_if_mounted_later_and_focused(): - """Check if full input value is rendered if the widget is mounted later and immediately focused.""" - - class MyApp(App): - BINDINGS = [("a", "add_input", "add_input")] - - async def action_add_input(self): - inp = Input(value="value") - await self.mount(inp) - inp.focus() - - app = MyApp() - async with app.run_test() as pilot: - await pilot.press("a") - await pilot.pause() - console = Console(width=5) - with console.capture() as capture: - console.print(app.query_one(Input).render()) - assert capture.get() == "value" - - -async def test_input_value_visible_if_mounted_later_and_assigned_after(): - """Check if full value rendered if the widget is mounted later and the value is then assigned to.""" - - class MyApp(App): - BINDINGS = [ - ("a", "add_input", "add_input"), - ("v", "set_value", "set_value"), - ] - - async def action_add_input(self): - await self.mount(Input()) - - def action_set_value(self): - self.query_one(Input).value = "value" - - app = MyApp() - async with app.run_test() as pilot: - await pilot.press("a") - await pilot.press("v") - console = Console(width=5) - with console.capture() as capture: - console.print(app.query_one(Input).render()) - assert capture.get() == "value" diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_ansi_command_palette.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_ansi_command_palette.svg index 806fe837e3..e8dbcb21f7 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_ansi_command_palette.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_ansi_command_palette.svg @@ -19,143 +19,143 @@ font-weight: 700; } - .terminal-2368587539-matrix { + .terminal-1724038555-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2368587539-title { + .terminal-1724038555-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2368587539-r1 { fill: #8a4346 } -.terminal-2368587539-r2 { fill: #868887 } -.terminal-2368587539-r3 { fill: #6b546f } -.terminal-2368587539-r4 { fill: #e0e0e0 } -.terminal-2368587539-r5 { fill: #292929 } -.terminal-2368587539-r6 { fill: #c5c8c6 } -.terminal-2368587539-r7 { fill: #0178d4 } -.terminal-2368587539-r8 { fill: #00ff00 } -.terminal-2368587539-r9 { fill: #000000 } -.terminal-2368587539-r10 { fill: #8d8d8d } -.terminal-2368587539-r11 { fill: #7e8486 } -.terminal-2368587539-r12 { fill: #e0e0e0;font-weight: bold } -.terminal-2368587539-r13 { fill: #a1a5a8 } + .terminal-1724038555-r1 { fill: #8a4346 } +.terminal-1724038555-r2 { fill: #868887 } +.terminal-1724038555-r3 { fill: #6b546f } +.terminal-1724038555-r4 { fill: #e0e0e0 } +.terminal-1724038555-r5 { fill: #292929 } +.terminal-1724038555-r6 { fill: #c5c8c6 } +.terminal-1724038555-r7 { fill: #0178d4 } +.terminal-1724038555-r8 { fill: #00ff00 } +.terminal-1724038555-r9 { fill: #000000 } +.terminal-1724038555-r10 { fill: #8d8d8d } +.terminal-1724038555-r11 { fill: #7e8486 } +.terminal-1724038555-r12 { fill: #e0e0e0;font-weight: bold } +.terminal-1724038555-r13 { fill: #a1a5a8 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - CommandPaletteApp + CommandPaletteApp - + - - RedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -🔎Search for commands… - - -  Quit the application                                                           -Quit the application as soon as possible -  Save screenshot                                                                -Save an SVG 'screenshot' of the current screen -  Show keys and help panel                                                       -Show help for the focused widget and a summary of available keys -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed + + RedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +🔎Search for commands… + + +  Quit the application                                                           +Quit the application as soon as possible +  Save screenshot                                                                +Save an SVG 'screenshot' of the current screen +  Show keys and help panel                                                       +Show help for the focused widget and a summary of available keys +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_blur.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_blur.svg index 87bfa8f0b8..186c3161ee 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_blur.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_blur.svg @@ -19,133 +19,133 @@ font-weight: 700; } - .terminal-1843476437-matrix { + .terminal-141898065-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1843476437-title { + .terminal-141898065-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1843476437-r1 { fill: #e0e0e0 } -.terminal-1843476437-r2 { fill: #c5c8c6 } -.terminal-1843476437-r3 { fill: #121212 } -.terminal-1843476437-r4 { fill: #191919 } + .terminal-141898065-r1 { fill: #e0e0e0 } +.terminal-141898065-r2 { fill: #c5c8c6 } +.terminal-141898065-r3 { fill: #121212 } +.terminal-141898065-r4 { fill: #191919 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AppBlurApp + AppBlurApp - + - - - - - - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -This should be the blur style      -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -This should also be the blur style -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +This should be the blur style      +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +his should also be the blur style  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_search_commands_opens_and_displays_search_list.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_search_commands_opens_and_displays_search_list.svg index 89e9f90784..b4d5e82f04 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_search_commands_opens_and_displays_search_list.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_search_commands_opens_and_displays_search_list.svg @@ -19,138 +19,138 @@ font-weight: 700; } - .terminal-1636454365-matrix { + .terminal-2118731365-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1636454365-title { + .terminal-2118731365-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1636454365-r1 { fill: #646464 } -.terminal-1636454365-r2 { fill: #c5c8c6 } -.terminal-1636454365-r3 { fill: #0178d4 } -.terminal-1636454365-r4 { fill: #e0e0e0 } -.terminal-1636454365-r5 { fill: #00ff00 } -.terminal-1636454365-r6 { fill: #000000 } -.terminal-1636454365-r7 { fill: #121212 } -.terminal-1636454365-r8 { fill: #e0e0e0;font-weight: bold } -.terminal-1636454365-r9 { fill: #e0e0e0;font-weight: bold;text-decoration: underline; } + .terminal-2118731365-r1 { fill: #646464 } +.terminal-2118731365-r2 { fill: #c5c8c6 } +.terminal-2118731365-r3 { fill: #0178d4 } +.terminal-2118731365-r4 { fill: #e0e0e0 } +.terminal-2118731365-r5 { fill: #00ff00 } +.terminal-2118731365-r6 { fill: #000000 } +.terminal-2118731365-r7 { fill: #121212 } +.terminal-2118731365-r8 { fill: #e0e0e0;font-weight: bold } +.terminal-2118731365-r9 { fill: #e0e0e0;font-weight: bold;text-decoration: underline; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SearchApp + SearchApp - + - - Search Commands                                                                  - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -🔎b - - -bar                                                                            -baz                                                                            -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - + + Search Commands                                                                  + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +🔎b + + +bar                                                                            +baz                                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_auto_grid.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_auto_grid.svg index 45ed109155..cb49a7cd50 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_auto_grid.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_auto_grid.svg @@ -19,135 +19,135 @@ font-weight: 700; } - .terminal-2473255221-matrix { + .terminal-84069957-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2473255221-title { + .terminal-84069957-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2473255221-r1 { fill: #008000 } -.terminal-2473255221-r2 { fill: #c5c8c6 } -.terminal-2473255221-r3 { fill: #e0e0e0 } -.terminal-2473255221-r4 { fill: #121212 } -.terminal-2473255221-r5 { fill: #191919 } + .terminal-84069957-r1 { fill: #008000 } +.terminal-84069957-r2 { fill: #c5c8c6 } +.terminal-84069957-r3 { fill: #e0e0e0 } +.terminal-84069957-r4 { fill: #121212 } +.terminal-84069957-r5 { fill: #191919 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - GridApp + GridApp - + - - ┌──────────────────────────────────────────────────────────────────────────────┐ -foo         ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -Longer label▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -└──────────────────────────────────────────────────────────────────────────────┘ -┌──────────────────────────────────────────────────────────────────────────────┐ -foo▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -Longer label▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -└──────────────────────────────────────────────────────────────────────────────┘ -┌──────────────────────────────────────────────────────────────────────────────┐ -foo bar foo bar foo bar foo ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -bar foo bar foo bar foo bar  -foo bar foo bar foo bar ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -Longer label                  ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -└──────────────────────────────────────────────────────────────────────────────┘ + + ┌──────────────────────────────────────────────────────────────────────────────┐ +foo         ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Longer label▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +foo▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Longer label▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +foo bar foo bar foo bar foo ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +bar foo bar foo bar foo bar  +foo bar foo bar foo bar ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Longer label                  ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_auto_width_input.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_auto_width_input.svg index f6cb5271b6..d38c62c1bd 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_auto_width_input.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_auto_width_input.svg @@ -19,136 +19,136 @@ font-weight: 700; } - .terminal-4171447127-matrix { + .terminal-1608004871-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4171447127-title { + .terminal-1608004871-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4171447127-r1 { fill: #c5c8c6 } -.terminal-4171447127-r2 { fill: #e0e0e0 } -.terminal-4171447127-r3 { fill: #121212 } -.terminal-4171447127-r4 { fill: #0178d4 } -.terminal-4171447127-r5 { fill: #495259 } -.terminal-4171447127-r6 { fill: #ffa62b;font-weight: bold } + .terminal-1608004871-r1 { fill: #c5c8c6 } +.terminal-1608004871-r2 { fill: #e0e0e0 } +.terminal-1608004871-r3 { fill: #121212 } +.terminal-1608004871-r4 { fill: #0178d4 } +.terminal-1608004871-r5 { fill: #495259 } +.terminal-1608004871-r6 { fill: #ffa62b;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - InputWidthAutoApp + InputWidthAutoApp - + - - ⭘                           InputWidthAutoApp                        -▔▔▔▔▔▔▔▔▔▔ -Hello -▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - - - -^p palette + + ⭘                           InputWidthAutoApp                        +▔▔▔▔▔▔▔▔▔▔ +Hello +▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + +^p palette diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_blur_on_disabled.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_blur_on_disabled.svg index d6049dbc2c..6ecdc017a4 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_blur_on_disabled.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_blur_on_disabled.svg @@ -19,134 +19,134 @@ font-weight: 700; } - .terminal-3443652466-matrix { + .terminal-4138202186-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3443652466-title { + .terminal-4138202186-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3443652466-r1 { fill: #121212 } -.terminal-3443652466-r2 { fill: #141414 } -.terminal-3443652466-r3 { fill: #c5c8c6 } -.terminal-3443652466-r4 { fill: #a2a2a2 } -.terminal-3443652466-r5 { fill: #e0e0e0 } + .terminal-4138202186-r1 { fill: #121212 } +.terminal-4138202186-r2 { fill: #141414 } +.terminal-4138202186-r3 { fill: #c5c8c6 } +.terminal-4138202186-r4 { fill: #a2a2a2 } +.terminal-4138202186-r5 { fill: #e0e0e0 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BlurApp + BlurApp - + - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -foo                                                                        -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - - - - + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +foo                                                                        +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_check_consume_keys.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_check_consume_keys.svg index 4ee1043c10..97b7ba33f9 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_check_consume_keys.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_check_consume_keys.svg @@ -19,140 +19,140 @@ font-weight: 700; } - .terminal-1966019072-matrix { + .terminal-18154040-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1966019072-title { + .terminal-18154040-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1966019072-r1 { fill: #121212 } -.terminal-1966019072-r2 { fill: #0178d4 } -.terminal-1966019072-r3 { fill: #c5c8c6 } -.terminal-1966019072-r4 { fill: #797979 } -.terminal-1966019072-r5 { fill: #e0e0e0 } -.terminal-1966019072-r6 { fill: #191919 } -.terminal-1966019072-r7 { fill: #737373 } -.terminal-1966019072-r8 { fill: #1e1e1e } -.terminal-1966019072-r9 { fill: #495259 } -.terminal-1966019072-r10 { fill: #ffa62b;font-weight: bold } + .terminal-18154040-r1 { fill: #121212 } +.terminal-18154040-r2 { fill: #0178d4 } +.terminal-18154040-r3 { fill: #c5c8c6 } +.terminal-18154040-r4 { fill: #797979 } +.terminal-18154040-r5 { fill: #e0e0e0 } +.terminal-18154040-r6 { fill: #191919 } +.terminal-18154040-r7 { fill: #737373 } +.terminal-18154040-r8 { fill: #1e1e1e } +.terminal-18154040-r9 { fill: #495259 } +.terminal-18154040-r10 { fill: #ffa62b;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + MyApp - + - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -First Name -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Last Name -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔ - -▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - -^p palette + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +First Name +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Last Name +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔ + +▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + +^p palette diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette.svg index cba485475e..c92c7f2d9d 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette.svg @@ -19,138 +19,138 @@ font-weight: 700; } - .terminal-566941328-matrix { + .terminal-3720858904-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-566941328-title { + .terminal-3720858904-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-566941328-r1 { fill: #646464 } -.terminal-566941328-r2 { fill: #c5c8c6 } -.terminal-566941328-r3 { fill: #0178d4 } -.terminal-566941328-r4 { fill: #e0e0e0 } -.terminal-566941328-r5 { fill: #00ff00 } -.terminal-566941328-r6 { fill: #000000 } -.terminal-566941328-r7 { fill: #121212 } -.terminal-566941328-r8 { fill: #e0e0e0;font-weight: bold } -.terminal-566941328-r9 { fill: #e0e0e0;font-weight: bold;text-decoration: underline; } + .terminal-3720858904-r1 { fill: #646464 } +.terminal-3720858904-r2 { fill: #c5c8c6 } +.terminal-3720858904-r3 { fill: #0178d4 } +.terminal-3720858904-r4 { fill: #e0e0e0 } +.terminal-3720858904-r5 { fill: #00ff00 } +.terminal-3720858904-r6 { fill: #000000 } +.terminal-3720858904-r7 { fill: #121212 } +.terminal-3720858904-r8 { fill: #e0e0e0;font-weight: bold } +.terminal-3720858904-r9 { fill: #e0e0e0;font-weight: bold;text-decoration: underline; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - CommandPaletteApp + CommandPaletteApp - + - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -🔎A - - -  This is a test of this code 9                                                  -  This is a test of this code 8                                                  -  This is a test of this code 7                                                  -  This is a test of this code 6                                                  -  This is a test of this code 5                                                  -  This is a test of this code 4                                                  -  This is a test of this code 3                                                  -  This is a test of this code 2                                                  -  This is a test of this code 1                                                  -  This is a test of this code 0                                                  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +🔎A + + +  This is a test of this code 9                                                  +  This is a test of this code 8                                                  +  This is a test of this code 7                                                  +  This is a test of this code 6                                                  +  This is a test of this code 5                                                  +  This is a test of this code 4                                                  +  This is a test of this code 3                                                  +  This is a test of this code 2                                                  +  This is a test of this code 1                                                  +  This is a test of this code 0                                                  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette_discovery.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette_discovery.svg index 60c08e5cee..48821c2c91 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette_discovery.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette_discovery.svg @@ -19,138 +19,138 @@ font-weight: 700; } - .terminal-1531744096-matrix { + .terminal-3222636008-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1531744096-title { + .terminal-3222636008-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1531744096-r1 { fill: #646464 } -.terminal-1531744096-r2 { fill: #c5c8c6 } -.terminal-1531744096-r3 { fill: #0178d4 } -.terminal-1531744096-r4 { fill: #e0e0e0 } -.terminal-1531744096-r5 { fill: #00ff00 } -.terminal-1531744096-r6 { fill: #000000 } -.terminal-1531744096-r7 { fill: #121212 } -.terminal-1531744096-r8 { fill: #6d7479 } -.terminal-1531744096-r9 { fill: #e0e0e0;font-weight: bold } + .terminal-3222636008-r1 { fill: #646464 } +.terminal-3222636008-r2 { fill: #c5c8c6 } +.terminal-3222636008-r3 { fill: #0178d4 } +.terminal-3222636008-r4 { fill: #e0e0e0 } +.terminal-3222636008-r5 { fill: #00ff00 } +.terminal-3222636008-r6 { fill: #000000 } +.terminal-3222636008-r7 { fill: #121212 } +.terminal-3222636008-r8 { fill: #6d7479 } +.terminal-3222636008-r9 { fill: #e0e0e0;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - CommandPaletteApp + CommandPaletteApp - + - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -🔎Search for commands… - - -  This is a test of this code 0                                                  -  This is a test of this code 1                                                  -  This is a test of this code 2                                                  -  This is a test of this code 3                                                  -  This is a test of this code 4                                                  -  This is a test of this code 5                                                  -  This is a test of this code 6                                                  -  This is a test of this code 7                                                  -  This is a test of this code 8                                                  -  This is a test of this code 9                                                  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +🔎Search for commands… + + +  This is a test of this code 0                                                  +  This is a test of this code 1                                                  +  This is a test of this code 2                                                  +  This is a test of this code 3                                                  +  This is a test of this code 4                                                  +  This is a test of this code 5                                                  +  This is a test of this code 6                                                  +  This is a test of this code 7                                                  +  This is a test of this code 8                                                  +  This is a test of this code 9                                                  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_example_dictionary.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_example_dictionary.svg index df1d21a58b..1faeefe670 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_example_dictionary.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_example_dictionary.svg @@ -19,135 +19,135 @@ font-weight: 700; } - .terminal-3533541262-matrix { + .terminal-576095766-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3533541262-title { + .terminal-576095766-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3533541262-r1 { fill: #e0e0e0 } -.terminal-3533541262-r2 { fill: #c5c8c6 } -.terminal-3533541262-r3 { fill: #242f38 } -.terminal-3533541262-r4 { fill: #0178d4 } -.terminal-3533541262-r5 { fill: #121212 } -.terminal-3533541262-r6 { fill: #797979 } + .terminal-576095766-r1 { fill: #e0e0e0 } +.terminal-576095766-r2 { fill: #c5c8c6 } +.terminal-576095766-r3 { fill: #242f38 } +.terminal-576095766-r4 { fill: #0178d4 } +.terminal-576095766-r5 { fill: #121212 } +.terminal-576095766-r6 { fill: #797979 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DictionaryApp + DictionaryApp - + - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Search for a word -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - - - - - - - - - - - - - - - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Search for a word +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + + + + + + + + + + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg index 39f191cb71..ce0c8ee333 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg @@ -19,161 +19,161 @@ font-weight: 700; } - .terminal-3307307900-matrix { + .terminal-3760089357-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3307307900-title { + .terminal-3760089357-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3307307900-r1 { fill: #121212 } -.terminal-3307307900-r2 { fill: #0178d4 } -.terminal-3307307900-r3 { fill: #4f4f4f } -.terminal-3307307900-r4 { fill: #c5c8c6 } -.terminal-3307307900-r5 { fill: #fea62b;font-weight: bold } -.terminal-3307307900-r6 { fill: #e0e0e0 } -.terminal-3307307900-r7 { fill: #8d8d8d } + .terminal-3760089357-r1 { fill: #121212 } +.terminal-3760089357-r2 { fill: #0178d4 } +.terminal-3760089357-r3 { fill: #4f4f4f } +.terminal-3760089357-r4 { fill: #c5c8c6 } +.terminal-3760089357-r5 { fill: #fea62b;font-weight: bold } +.terminal-3760089357-r6 { fill: #e0e0e0 } +.terminal-3760089357-r7 { fill: #000000 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - HelpPanelApp + HelpPanelApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -        ←Move cursor left   -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁       ^←Move cursor left a -word               -        →Move cursor right  -       ^→Move cursor right  -a word             -        ⌫Delete character   -left               -  home ^aGo to start        -   end ^eGo to end          -   del ^dDelete character   -right              -        ⏎Submit             -       ^wDelete left to     -start of word      -       ^uDelete all to the  -left               -       ^fDelete right to    -start of word      -       ^kDelete all to the  -right              - -      tabFocus Next         -shift+tabFocus Previous     - -       ^cQuit               -       ^ppalette Open  -command palette - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +        ↑Scroll Up        +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁        ↓Scroll Down      +        ←Move cursor left +        →Move cursor      +right            +  home ^aGo to start      +   end ^eGo to end        +     pgupPage Up          +     pgdnPage Down        +    ^pgupPage Left        +    ^pgdnPage Right       +  shift+←Move cursor left +and select       +       ^←Move cursor left +a word           + shift+^←Move cursor left +a word and       +select           +  shift+→Move cursor     ▆▆ +right and select +       ^→Move cursor      +right a word     + shift+^→Move cursor      +right a word and +select           +        ⌫Delete character +left             +   del ^dDelete character +right            diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_and_focus.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_and_focus.svg index 3561a95691..60366fbe9c 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_and_focus.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_and_focus.svg @@ -19,134 +19,134 @@ font-weight: 700; } - .terminal-1525652049-matrix { + .terminal-2998242737-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1525652049-title { + .terminal-2998242737-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1525652049-r1 { fill: #121212 } -.terminal-1525652049-r2 { fill: #191919 } -.terminal-1525652049-r3 { fill: #c5c8c6 } -.terminal-1525652049-r4 { fill: #e0e0e0 } -.terminal-1525652049-r5 { fill: #0178d4 } + .terminal-2998242737-r1 { fill: #121212 } +.terminal-2998242737-r2 { fill: #191919 } +.terminal-2998242737-r3 { fill: #c5c8c6 } +.terminal-2998242737-r4 { fill: #e0e0e0 } +.terminal-2998242737-r5 { fill: #0178d4 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - InputApp + InputApp - + - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Darren                                                                     -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Burns -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Darren                                                                     +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Burns +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_percentage_width.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_percentage_width.svg index 9105a76248..ce82ac42e6 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_percentage_width.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_percentage_width.svg @@ -19,135 +19,135 @@ font-weight: 700; } - .terminal-2301157416-matrix { + .terminal-4155444696-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2301157416-title { + .terminal-4155444696-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2301157416-r1 { fill: #121212 } -.terminal-2301157416-r2 { fill: #e0e0e0 } -.terminal-2301157416-r3 { fill: #c5c8c6 } -.terminal-2301157416-r4 { fill: #ff0000 } -.terminal-2301157416-r5 { fill: #e0e0e0;font-weight: bold } + .terminal-4155444696-r1 { fill: #121212 } +.terminal-4155444696-r2 { fill: #e0e0e0 } +.terminal-4155444696-r3 { fill: #c5c8c6 } +.terminal-4155444696-r4 { fill: #ff0000 } +.terminal-4155444696-r5 { fill: #e0e0e0;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - InputVsTextArea + InputVsTextArea - - - - 01234567890123456789012345678901234567890123456789012345678901234567890123456789 -┌──────────────────────────────────────┐ - - - -└──────────────────────────────────────┘ -┌──────────────────────────────────────┐ - - - - -└──────────────────────────────────────┘ -┌──────────────────────────────────────┐ - - - - -└──────────────────────────────────────┘ -┌──────────────────────────────────────┐ - - Button  - - -└──────────────────────────────────────┘ + + + + 01234567890123456789012345678901234567890123456789012345678901234567890123456789 +┌──────────────────────────────────────┐ + + + +└──────────────────────────────────────┘ +┌──────────────────────────────────────┐ + + + + +└──────────────────────────────────────┘ +┌──────────────────────────────────────┐ + + + + +└──────────────────────────────────────┘ +┌──────────────────────────────────────┐ + + Button  + + +└──────────────────────────────────────┘ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_suggestions.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_suggestions.svg index 36b75f3525..0bf720ec6b 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_suggestions.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_suggestions.svg @@ -19,136 +19,136 @@ font-weight: 700; } - .terminal-1684740891-matrix { + .terminal-1035141595-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1684740891-title { + .terminal-1035141595-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1684740891-r1 { fill: #121212 } -.terminal-1684740891-r2 { fill: #0178d4 } -.terminal-1684740891-r3 { fill: #c5c8c6 } -.terminal-1684740891-r4 { fill: #e0e0e0 } -.terminal-1684740891-r5 { fill: #121212;font-style: italic; } -.terminal-1684740891-r6 { fill: #ff0000;font-style: italic; } -.terminal-1684740891-r7 { fill: #191919 } + .terminal-1035141595-r1 { fill: #121212 } +.terminal-1035141595-r2 { fill: #0178d4 } +.terminal-1035141595-r3 { fill: #c5c8c6 } +.terminal-1035141595-r4 { fill: #e0e0e0 } +.terminal-1035141595-r5 { fill: #121212;font-style: italic; } +.terminal-1035141595-r6 { fill: #ff0000;font-style: italic; } +.terminal-1035141595-r7 { fill: #191919 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - FruitsApp + FruitsApp - + - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -strawberry -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -straw                                                                      -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -p                                                                          -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -b                                                                          -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -a                                                                          -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +strawberry +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +straw                                                                      +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +p                                                                          +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +b                                                                          +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +a                                                                          +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_validation.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_validation.svg index c72c40b4a2..4bb28e0f53 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_validation.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_validation.svg @@ -19,137 +19,137 @@ font-weight: 700; } - .terminal-3009702123-matrix { + .terminal-1570722515-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3009702123-title { + .terminal-1570722515-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3009702123-r1 { fill: #e0e0e0 } -.terminal-3009702123-r2 { fill: #c5c8c6 } -.terminal-3009702123-r3 { fill: #121212 } -.terminal-3009702123-r4 { fill: #762b3d } -.terminal-3009702123-r5 { fill: #36794b } -.terminal-3009702123-r6 { fill: #b93c5b } -.terminal-3009702123-r7 { fill: #191919 } -.terminal-3009702123-r8 { fill: #737373 } + .terminal-1570722515-r1 { fill: #e0e0e0 } +.terminal-1570722515-r2 { fill: #c5c8c6 } +.terminal-1570722515-r3 { fill: #121212 } +.terminal-1570722515-r4 { fill: #762b3d } +.terminal-1570722515-r5 { fill: #36794b } +.terminal-1570722515-r6 { fill: #b93c5b } +.terminal-1570722515-r7 { fill: #191919 } +.terminal-1570722515-r8 { fill: #737373 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - InputApp + InputApp - + - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ --2                                                                     -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -3                                                                      -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ --2 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Enter a number between 1 and 5 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +-2                                                                     +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +3                                                                      +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +-2 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Enter a number between 1 and 5 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_layout_containers.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_layout_containers.svg index 4a2a93e807..d7a9d8825a 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_layout_containers.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_layout_containers.svg @@ -19,143 +19,143 @@ font-weight: 700; } - .terminal-2895197884-matrix { + .terminal-2364090908-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2895197884-title { + .terminal-2364090908-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2895197884-r1 { fill: #7ae998 } -.terminal-2895197884-r2 { fill: #e76580 } -.terminal-2895197884-r3 { fill: #121212 } -.terminal-2895197884-r4 { fill: #191919 } -.terminal-2895197884-r5 { fill: #c5c8c6 } -.terminal-2895197884-r6 { fill: #55c076;font-weight: bold } -.terminal-2895197884-r7 { fill: #f5e5e9;font-weight: bold } -.terminal-2895197884-r8 { fill: #e0e0e0 } -.terminal-2895197884-r9 { fill: #0a180e;font-weight: bold } -.terminal-2895197884-r10 { fill: #008139 } -.terminal-2895197884-r11 { fill: #780028 } -.terminal-2895197884-r12 { fill: #003054 } -.terminal-2895197884-r13 { fill: #000000 } + .terminal-2364090908-r1 { fill: #7ae998 } +.terminal-2364090908-r2 { fill: #e76580 } +.terminal-2364090908-r3 { fill: #121212 } +.terminal-2364090908-r4 { fill: #191919 } +.terminal-2364090908-r5 { fill: #c5c8c6 } +.terminal-2364090908-r6 { fill: #55c076;font-weight: bold } +.terminal-2364090908-r7 { fill: #f5e5e9;font-weight: bold } +.terminal-2364090908-r8 { fill: #e0e0e0 } +.terminal-2364090908-r9 { fill: #0a180e;font-weight: bold } +.terminal-2364090908-r10 { fill: #008139 } +.terminal-2364090908-r11 { fill: #780028 } +.terminal-2364090908-r12 { fill: #003054 } +.terminal-2364090908-r13 { fill: #000000 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + MyApp - + - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Accept  Decline  Accept  Decline  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Accept  Accept  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Decline  Decline  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▆▆ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -0                                 0 - -1000000                                 1000000                                + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Accept  Decline  Accept  Decline  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Accept  Accept  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Decline  Decline  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▆▆ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +0                                 0 + +1000000                                 1000000                                diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_markup_command_list.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_markup_command_list.svg index 3e2a84d61c..66eac747ca 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_markup_command_list.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_markup_command_list.svg @@ -19,141 +19,141 @@ font-weight: 700; } - .terminal-796303638-matrix { + .terminal-2832635806-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-796303638-title { + .terminal-2832635806-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-796303638-r1 { fill: #646464 } -.terminal-796303638-r2 { fill: #c5c8c6 } -.terminal-796303638-r3 { fill: #0178d4 } -.terminal-796303638-r4 { fill: #e0e0e0 } -.terminal-796303638-r5 { fill: #00ff00 } -.terminal-796303638-r6 { fill: #000000 } -.terminal-796303638-r7 { fill: #121212 } -.terminal-796303638-r8 { fill: #6d7479 } -.terminal-796303638-r9 { fill: #e0e0e0;font-weight: bold } -.terminal-796303638-r10 { fill: #98e024;font-weight: bold;text-decoration: underline; } -.terminal-796303638-r11 { fill: #a1a5a8 } -.terminal-796303638-r12 { fill: #f4005f;text-decoration: underline; } + .terminal-2832635806-r1 { fill: #646464 } +.terminal-2832635806-r2 { fill: #c5c8c6 } +.terminal-2832635806-r3 { fill: #0178d4 } +.terminal-2832635806-r4 { fill: #e0e0e0 } +.terminal-2832635806-r5 { fill: #00ff00 } +.terminal-2832635806-r6 { fill: #000000 } +.terminal-2832635806-r7 { fill: #121212 } +.terminal-2832635806-r8 { fill: #6d7479 } +.terminal-2832635806-r9 { fill: #e0e0e0;font-weight: bold } +.terminal-2832635806-r10 { fill: #98e024;font-weight: bold;text-decoration: underline; } +.terminal-2832635806-r11 { fill: #a1a5a8 } +.terminal-2832635806-r12 { fill: #f4005f;text-decoration: underline; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + MyApp - + - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -🔎Search for commands… - - -  Hello World -Help text -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +🔎Search for commands… + + +  Hello World +Help text +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_masked_input.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_masked_input.svg index 60b0341821..342425934a 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_masked_input.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_masked_input.svg @@ -19,136 +19,136 @@ font-weight: 700; } - .terminal-3304533527-matrix { + .terminal-324016935-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3304533527-title { + .terminal-324016935-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3304533527-r1 { fill: #121212 } -.terminal-3304533527-r2 { fill: #b93c5b } -.terminal-3304533527-r3 { fill: #c5c8c6 } -.terminal-3304533527-r4 { fill: #e0e0e0 } -.terminal-3304533527-r5 { fill: #797979 } -.terminal-3304533527-r6 { fill: #191919 } -.terminal-3304533527-r7 { fill: #737373 } + .terminal-324016935-r1 { fill: #121212 } +.terminal-324016935-r2 { fill: #b93c5b } +.terminal-324016935-r3 { fill: #c5c8c6 } +.terminal-324016935-r4 { fill: #e0e0e0 } +.terminal-324016935-r5 { fill: #797979 } +.terminal-324016935-r6 { fill: #191919 } +.terminal-324016935-r7 { fill: #737373 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TemplateApp + TemplateApp - + - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -ABC01-DE___-_____-_____ -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -YYYY-MM-DD -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +ABC01-DE___-_____-_____ +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +YYYY-MM-DD +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_modal_dialog_bindings_input.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_modal_dialog_bindings_input.svg index ee2f5a5d5b..65aa707c03 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_modal_dialog_bindings_input.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_modal_dialog_bindings_input.svg @@ -19,140 +19,140 @@ font-weight: 700; } - .terminal-2884096318-matrix { + .terminal-918474262-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2884096318-title { + .terminal-918474262-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2884096318-r1 { fill: #e0e0e0 } -.terminal-2884096318-r2 { fill: #646464 } -.terminal-2884096318-r3 { fill: #c5c8c6 } -.terminal-2884096318-r4 { fill: #121212 } -.terminal-2884096318-r5 { fill: #191919 } -.terminal-2884096318-r6 { fill: #2d2d2d } -.terminal-2884096318-r7 { fill: #272727;font-weight: bold } -.terminal-2884096318-r8 { fill: #0d0d0d } -.terminal-2884096318-r9 { fill: #704d1c;font-weight: bold } -.terminal-2884096318-r10 { fill: #282b2e } + .terminal-918474262-r1 { fill: #e0e0e0 } +.terminal-918474262-r2 { fill: #646464 } +.terminal-918474262-r3 { fill: #c5c8c6 } +.terminal-918474262-r4 { fill: #121212 } +.terminal-918474262-r5 { fill: #191919 } +.terminal-918474262-r6 { fill: #2d2d2d } +.terminal-918474262-r7 { fill: #272727;font-weight: bold } +.terminal-918474262-r8 { fill: #0d0d0d } +.terminal-918474262-r9 { fill: #704d1c;font-weight: bold } +.terminal-918474262-r10 { fill: #282b2e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ModalApp + ModalApp - + - - Dialog                           ModalApp                            -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -hi!                                                                        -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - OK  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - ⏎ Open Dialog                                                      ^p palette + + Dialog                           ModalApp                            +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +hi!                                                                        +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + OK  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + ⏎ Open Dialog                                                      ^p palette diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_system_commands.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_system_commands.svg index dbb425da5e..d34346af86 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_system_commands.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_system_commands.svg @@ -19,164 +19,164 @@ font-weight: 700; } - .terminal-3076581726-matrix { + .terminal-196009958-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3076581726-title { + .terminal-196009958-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3076581726-r1 { fill: #121212 } -.terminal-3076581726-r2 { fill: #0b3a5f } -.terminal-3076581726-r3 { fill: #c5c8c6 } -.terminal-3076581726-r4 { fill: #e0e0e0 } -.terminal-3076581726-r5 { fill: #0178d4 } -.terminal-3076581726-r6 { fill: #00ff00 } -.terminal-3076581726-r7 { fill: #000000 } -.terminal-3076581726-r8 { fill: #6d7479 } -.terminal-3076581726-r9 { fill: #e0e0e0;font-weight: bold } -.terminal-3076581726-r10 { fill: #a1a5a8 } -.terminal-3076581726-r11 { fill: #646464 } + .terminal-196009958-r1 { fill: #121212 } +.terminal-196009958-r2 { fill: #0b3a5f } +.terminal-196009958-r3 { fill: #c5c8c6 } +.terminal-196009958-r4 { fill: #e0e0e0 } +.terminal-196009958-r5 { fill: #0178d4 } +.terminal-196009958-r6 { fill: #00ff00 } +.terminal-196009958-r7 { fill: #000000 } +.terminal-196009958-r8 { fill: #6d7479 } +.terminal-196009958-r9 { fill: #e0e0e0;font-weight: bold } +.terminal-196009958-r10 { fill: #a1a5a8 } +.terminal-196009958-r11 { fill: #646464 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SimpleApp + SimpleApp - + - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -🔎Search for commands… - - -  Change theme                                                                                       -Change the current theme -  Maximize                                                                                           -Maximize the focused widget -  Quit the application                                                                               -Quit the application as soon as possible -  Save screenshot                                                                                    -Save an SVG 'screenshot' of the current screen -  Show keys and help panel                                                                           -Show help for the focused widget and a summary of available keys -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +🔎Search for commands… + + +  Change theme                                                                                       +Change the current theme +  Maximize                                                                                           +Maximize the focused widget +  Quit the application                                                                               +Quit the application as soon as possible +  Save screenshot                                                                                    +Save an SVG 'screenshot' of the current screen +  Show keys and help panel                                                                           +Show help for the focused widget and a summary of available keys +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_textual_dev_easing_preview.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_textual_dev_easing_preview.svg index 3498ecdc0f..dc1e871e4e 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_textual_dev_easing_preview.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_textual_dev_easing_preview.svg @@ -19,146 +19,146 @@ font-weight: 700; } - .terminal-792837549-matrix { + .terminal-1734916741-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-792837549-title { + .terminal-1734916741-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-792837549-r1 { fill: #2d2d2d } -.terminal-792837549-r2 { fill: #121212 } -.terminal-792837549-r3 { fill: #c5c8c6 } -.terminal-792837549-r4 { fill: #272727;font-weight: bold } -.terminal-792837549-r5 { fill: #1b1b1b } -.terminal-792837549-r6 { fill: #e0e0e0 } -.terminal-792837549-r7 { fill: #0d0d0d } -.terminal-792837549-r8 { fill: #e0e0e0;font-weight: bold } -.terminal-792837549-r9 { fill: #000000 } -.terminal-792837549-r10 { fill: #1e1e1e } -.terminal-792837549-r11 { fill: #b93c5b } -.terminal-792837549-r12 { fill: #fea62b } -.terminal-792837549-r13 { fill: #211505;font-weight: bold } -.terminal-792837549-r14 { fill: #211505 } -.terminal-792837549-r15 { fill: #495259 } -.terminal-792837549-r16 { fill: #ffa62b;font-weight: bold } + .terminal-1734916741-r1 { fill: #2d2d2d } +.terminal-1734916741-r2 { fill: #121212 } +.terminal-1734916741-r3 { fill: #c5c8c6 } +.terminal-1734916741-r4 { fill: #272727;font-weight: bold } +.terminal-1734916741-r5 { fill: #1b1b1b } +.terminal-1734916741-r6 { fill: #e0e0e0 } +.terminal-1734916741-r7 { fill: #0d0d0d } +.terminal-1734916741-r8 { fill: #e0e0e0;font-weight: bold } +.terminal-1734916741-r9 { fill: #000000 } +.terminal-1734916741-r10 { fill: #1e1e1e } +.terminal-1734916741-r11 { fill: #b93c5b } +.terminal-1734916741-r12 { fill: #fea62b } +.terminal-1734916741-r13 { fill: #211505;font-weight: bold } +.terminal-1734916741-r14 { fill: #211505 } +.terminal-1734916741-r15 { fill: #495259 } +.terminal-1734916741-r16 { fill: #ffa62b;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - EasingApp + EasingApp - + - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - round ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Animation Duration:1.0                        -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - out_sine  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - out_quint  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Welcome to Textual! -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - out_quart I must not fear. -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Fear is the  -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔mind-killer. - out_quad Fear is the  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁little-death that  -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔brings total  - out_expo obliteration. -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I will face my fear. -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔I will permit it to  - out_elastic pass over me and  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁through me. -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔And when it has gone  - out_cubic  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁^p palette + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + round ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Animation Duration:1.0                        +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + out_sine  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + out_quint  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Welcome to Textual! +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + out_quart I must not fear. +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Fear is the  +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔mind-killer. + out_quad Fear is the  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁little-death that  +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔brings total  + out_expo obliteration. +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I will face my fear. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔I will permit it to  + out_elastic pass over me and  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁through me. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔And when it has gone  + out_cubic  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁^p palette From 8dc81acb1d1606eb774728f6d11da01e9d8fdb20 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 12:00:12 +0000 Subject: [PATCH 14/34] Snapshot tests for Input --- tests/snapshot_tests/test_snapshots.py | 77 ++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index caa74cb124..0944845991 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -144,6 +144,83 @@ async def run_before(pilot): ) +def test_input_setting_value(snap_compare): + """Test that Inputs with different values are rendered correctly. + + The values of inputs should be (from top to bottom): "default", "set attribute in compose" + , "" (empty), a placeholder of 'Placeholder, no value', and "set in on_mount". + """ + + class InputApp(App[None]): + def compose(self) -> ComposeResult: + yield Input(value="default") + input2 = Input() + input2.value = "set attribute in compose" + yield input2 + yield Input() + yield Input(placeholder="Placeholder, no value") + yield Input(id="input3") + + def on_mount(self) -> None: + input3 = self.query_one("#input3", Input) + input3.value = "set in on_mount" + + assert snap_compare(InputApp()) + + +def test_input_cursor(snap_compare): + """The first input should say こんにちは. + The second input should say こんにちは, with a cursor on the final character (double width). + Note that this might render incorrectly in the SVG output - the letters may overlap.""" + + class InputApp(App[None]): + def compose(self) -> ComposeResult: + yield Input(value="こんにちは") + input = Input(value="こんにちは") + input.focus() + input.action_cursor_left() + yield input + + assert snap_compare(InputApp()) + + +def test_input_scrolls_to_cursor(snap_compare): + """The input widget should scroll the cursor into view when it moves, + and this should account for different cell widths. + + Only the final two characters should be visible in the first input (ちは). + They might be overlapping in the SVG output. + + In the second input, we should only see numbers 5-9 inclusive, plus the cursor. + The number of cells to the right of the cursor should equal the number of cells + to the left of the number '5'. + """ + + class InputScrollingApp(App[None]): + CSS = "Input { width: 12; }" + + def compose(self) -> ComposeResult: + yield Input(id="input1") + yield Input(id="input2") + + assert snap_compare( + InputScrollingApp(), press=[*"こんにちは", "tab", *"0123456789"] + ) + + +def test_input_selection(snap_compare): + """BCDEF should be visible, and DEF should be selected. The cursor should be + sitting above 'D'.""" + + class InputSelectionApp(App[None]): + CSS = "Input { width: 12; }" + + def compose(self) -> ComposeResult: + yield Input(id="input1") + + assert snap_compare(InputSelectionApp(), press=[*"ABCDEF", *("shift+left",) * 3]) + + def test_masked_input(snap_compare): async def run_before(pilot): pilot.app.query(Input).first().cursor_blink = False From c72d85f7353ae0956400edbce5556fb2fb181b39 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 12:00:52 +0000 Subject: [PATCH 15/34] Creating new snapshots --- .../test_snapshots/test_input_cursor.svg | 153 +++++++++++++++++ .../test_input_scrolls_to_cursor.svg | 153 +++++++++++++++++ .../test_snapshots/test_input_selection.svg | 152 +++++++++++++++++ .../test_input_setting_value.svg | 154 ++++++++++++++++++ 4 files changed, 612 insertions(+) create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_input_cursor.svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_input_scrolls_to_cursor.svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_input_selection.svg create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_input_setting_value.svg diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_cursor.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_cursor.svg new file mode 100644 index 0000000000..5086f54c9e --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_cursor.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + InputApp + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +こんにちは                                                                 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +こんにち +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_scrolls_to_cursor.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_scrolls_to_cursor.svg new file mode 100644 index 0000000000..6ce95f3575 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_scrolls_to_cursor.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + InputScrollingApp + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔ + ちは  +▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔ +56789 +▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_selection.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_selection.svg new file mode 100644 index 0000000000..21ee18096d --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_selection.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + InputSelectionApp + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔ +BCDEF +▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_setting_value.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_setting_value.svg new file mode 100644 index 0000000000..440d5dba1d --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_setting_value.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + InputApp + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +default +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +set attribute in compose                                                   +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Placeholder, no value +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +set in on_mount                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + From ff5412587a34f1d4aec5824668216ce01d13ed42 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 13:12:24 +0000 Subject: [PATCH 16/34] Simplifying implementations --- src/textual/widgets/_input.py | 57 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 6b53c7f6c1..bf3294bb79 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -864,14 +864,25 @@ def action_cursor_right_word(self, select: bool = False) -> None: else: self.cursor_position = target + def replace(self, text: str, start: int, end: int) -> None: + """Replace the text between the start and end locations with the given text.""" + start, end = sorted((max(0, start), min(len(self.value), end))) + self.value = f"{self.value[:start]}{text}{self.value[end:]}" + self.cursor_position = start + len(text) + + def delete(self, start: int, end: int) -> None: + """Delete the text between the start and end locations.""" + start, end = sorted((start, end)) + self.replace("", start, end) + def action_delete_right(self) -> None: """Delete one character at the current cursor position.""" - 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 + if self.selection.empty: + start, end = self.cursor_position, self.cursor_position + 1 + else: + start, end = self.selection + + self.delete(start, end) def action_delete_right_word(self) -> None: """Delete the current character and all rightward to the start of the next word.""" @@ -883,34 +894,24 @@ def action_delete_right_word(self) -> None: after = self.value[self.cursor_position :] hit = re.search(self._WORD_START, after) if hit is None: - self.value = self.value[: self.cursor_position] + self.action_delete_right_all() else: - self.value = ( - f"{self.value[: self.cursor_position]}{after[hit.end() - 1:]}" - ) + start = self.cursor_position + end = start + hit.end() - 1 + self.delete(start, end) def action_delete_right_all(self) -> None: """Delete the current character and all characters to the right of the cursor position.""" - self.value = self.value[: self.cursor_position] + self.delete(self.cursor_position, len(self.value)) def action_delete_left(self) -> None: """Delete one character to the left of the current cursor position.""" - if self.cursor_position <= 0: - # Cursor at the start, so nothing to delete - return - - if self._cursor_at_end: - # Delete from end - self.value = self.value[:-1] - self.cursor_position = len(self.value) + if self.selection.empty: + start, end = self.cursor_position - 1, self.cursor_position else: - # Cursor in the middle - value = self.value - delete_position = self.cursor_position - 1 - before = value[:delete_position] - after = value[delete_position + 1 :] - self.value = f"{before}{after}" - self.cursor_position = delete_position + start, end = self.selection + + self.delete(start, end) def action_delete_left_word(self) -> None: """Delete leftward of the cursor position to the start of a word.""" @@ -935,9 +936,7 @@ def action_delete_left_word(self) -> None: def action_delete_left_all(self) -> None: """Delete all characters to the left of the cursor position.""" - if self.cursor_position > 0: - self.value = self.value[self.cursor_position :] - self.cursor_position = 0 + self.delete(0, self.cursor_position) async def action_submit(self) -> None: """Handle a submit action. From 1788ca84e7630ff852b9181893e0f062b6b68771 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 13:19:57 +0000 Subject: [PATCH 17/34] More simplification --- src/textual/widgets/_input.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index bf3294bb79..412c69f312 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -872,20 +872,25 @@ def replace(self, text: str, start: int, end: int) -> None: def delete(self, start: int, end: int) -> None: """Delete the text between the start and end locations.""" - start, end = sorted((start, end)) self.replace("", start, end) + def delete_selection(self) -> None: + """Delete the current selection.""" + self.delete(*self.selection) + def action_delete_right(self) -> None: """Delete one character at the current cursor position.""" if self.selection.empty: - start, end = self.cursor_position, self.cursor_position + 1 + self.delete(self.cursor_position, self.cursor_position + 1) else: - start, end = self.selection - - self.delete(start, end) + self.delete_selection() def action_delete_right_word(self) -> None: """Delete the current character and all rightward to the start of the next word.""" + if not self.selection.empty: + self.delete_selection() + return + if self.password: # This is a password field so don't give any hints about word # boundaries, even during deletion. @@ -902,16 +907,17 @@ def action_delete_right_word(self) -> None: def action_delete_right_all(self) -> None: """Delete the current character and all characters to the right of the cursor position.""" - self.delete(self.cursor_position, len(self.value)) + if self.selection.empty: + self.delete(self.cursor_position, len(self.value)) + else: + self.delete_selection() def action_delete_left(self) -> None: """Delete one character to the left of the current cursor position.""" if self.selection.empty: - start, end = self.cursor_position - 1, self.cursor_position + self.delete(self.cursor_position - 1, self.cursor_position) else: - start, end = self.selection - - self.delete(start, end) + self.delete_selection() def action_delete_left_word(self) -> None: """Delete leftward of the cursor position to the start of a word.""" From 5aa06c258e30e2d2b2fac3b862f5987e8f26eed0 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 13:30:33 +0000 Subject: [PATCH 18/34] Simplify delete_word_left --- src/textual/widgets/_input.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 412c69f312..eb7ca58500 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -865,7 +865,13 @@ def action_cursor_right_word(self, select: bool = False) -> None: self.cursor_position = target def replace(self, text: str, start: int, end: int) -> None: - """Replace the text between the start and end locations with the given text.""" + """Replace the text between the start and end locations with the given text. + + Args: + text: Text to replace the existing text with. + start: Start index to replace (inclusive). + end: End index to replace (inclusive). + """ start, end = sorted((max(0, start), min(len(self.value), end))) self.value = f"{self.value[:start]}{text}{self.value[end:]}" self.cursor_position = start + len(text) @@ -921,28 +927,32 @@ def action_delete_left(self) -> None: def action_delete_left_word(self) -> None: """Delete leftward of the cursor position to the start of a word.""" - if self.cursor_position <= 0: + if not self.selection.empty: + self.delete_selection() return + if self.password: # This is a password field so don't give any hints about word # boundaries, even during deletion. self.action_delete_left_all() else: - after = self.value[self.cursor_position :] try: *_, hit = re.finditer( self._WORD_START, self.value[: self.cursor_position] ) except ValueError: - self.cursor_position = 0 + target = 0 else: - self.cursor_position = hit.start() - new_value = f"{self.value[: self.cursor_position]}{after}" - self.value = new_value + target = hit.start() + + self.delete(target, self.cursor_position) def action_delete_left_all(self) -> None: """Delete all characters to the left of the cursor position.""" - self.delete(0, self.cursor_position) + if self.selection.empty: + self.delete(0, self.cursor_position) + else: + self.delete_selection() async def action_submit(self) -> None: """Handle a submit action. From c3339df6078e92494e52f5841c9c0956356438af Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 14:19:38 +0000 Subject: [PATCH 19/34] Apply Input.restrict to all edits --- src/textual/widgets/_input.py | 88 ++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index eb7ca58500..a70895a271 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -202,7 +202,7 @@ class Input(ScrollView): cursor_blink = reactive(True, init=False) # TODO - check with width: auto to see if layout=True is needed - value = reactive("", init=False) + value: Reactive[str] = reactive("", init=False) @property def cursor_position(self) -> int: @@ -712,43 +712,7 @@ def insert_text_at_cursor(self, text: str) -> None: Args: text: New text to insert. """ - - def check_allowed_value(value: str) -> bool: - """Check if new value is restricted.""" - # Check max length - if self.max_length and len(value) > self.max_length: - return False - # Check explicit restrict - if self.restrict and re.fullmatch(self.restrict, value) is None: - return False - # Check type restrict - if self.type: - type_restrict = _RESTRICT_TYPES.get(self.type, None) - if ( - type_restrict is not None - and re.fullmatch(type_restrict, value) is None - ): - return False - # Character is allowed - return True - - if self.cursor_position >= len(self.value): - new_value = self.value + text - if check_allowed_value(new_value): - self.value = new_value - self.cursor_position = len(self.value) - else: - self.restricted() - else: - value = self.value - before = value[: self.cursor_position] - after = value[self.cursor_position :] - new_value = f"{before}{text}{after}" - if check_allowed_value(new_value): - self.value = new_value - self.cursor_position += len(text) - else: - self.restricted() + self.insert(text, self.cursor_position) def restricted(self) -> None: """Called when a character has been restricted. @@ -872,12 +836,52 @@ def replace(self, text: str, start: int, end: int) -> None: start: Start index to replace (inclusive). end: End index to replace (inclusive). """ - start, end = sorted((max(0, start), min(len(self.value), end))) - self.value = f"{self.value[:start]}{text}{self.value[end:]}" - self.cursor_position = start + len(text) + + def check_allowed_value(value: str) -> bool: + """Check if new value is restricted.""" + + # Check max length + if self.max_length and len(value) > self.max_length: + return False + # Check explicit restrict + if self.restrict and re.fullmatch(self.restrict, value) is None: + return False + # Check type restrict + if self.type: + type_restrict = _RESTRICT_TYPES.get(self.type, None) + if ( + type_restrict is not None + and re.fullmatch(type_restrict, value) is None + ): + return False + # Character is allowed + return True + + value = self.value + start, end = sorted((max(0, start), min(len(value), end))) + new_value = f"{value[:start]}{text}{value[end:]}" + if check_allowed_value(new_value): + self.value = new_value + self.cursor_position = start + len(text) + else: + self.restricted() + + def insert(self, text: str, index: int) -> None: + """Insert text at the given index. + + Args: + text: Text to insert. + index: Index to insert the text at (inclusive). + """ + self.replace(text, index, index) def delete(self, start: int, end: int) -> None: - """Delete the text between the start and end locations.""" + """Delete the text between the start and end locations. + + Args: + start: Start index to delete (inclusive). + end: End index to delete (inclusive). + """ self.replace("", start, end) def delete_selection(self) -> None: From 7a7e553de384c3f155cec76b656547e5ad145371 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 14:27:44 +0000 Subject: [PATCH 20/34] Ensure interaction between selections and insertions (paste and insert) is correct --- src/textual/widgets/_input.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index a70895a271..792680a69f 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -637,13 +637,21 @@ async def _on_key(self, event: events.Key) -> None: if event.is_printable: event.stop() assert event.character is not None - self.insert_text_at_cursor(event.character) + selection = self.selection + if selection.empty: + self.insert_text_at_cursor(event.character) + else: + self.replace(event.character, *selection) event.prevent_default() def _on_paste(self, event: events.Paste) -> None: if event.text: line = event.text.splitlines()[0] - self.insert_text_at_cursor(line) + selection = self.selection + if selection.empty: + self.insert_text_at_cursor(line) + else: + self.replace(line, *selection) event.stop() def _cell_offset_to_index(self, offset: int) -> int: From a94971724eaf12e2f762ce94fac9323f5c2146bf Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 14:50:24 +0000 Subject: [PATCH 21/34] Add shift+home and shift+end bindings to Input --- src/textual/widgets/_input.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 792680a69f..d0f4b277a5 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -102,6 +102,8 @@ class Input(ScrollView): Binding("backspace", "delete_left", "Delete character left", show=False), Binding("home,ctrl+a", "home", "Go to start", show=False), Binding("end,ctrl+e", "end", "Go to end", show=False), + Binding("shift+home", "home(True)", "Select line start", show=False), + Binding("shift+end", "end(True)", "Select line end", show=False), Binding("delete,ctrl+d", "delete_right", "Delete character right", show=False), Binding("enter", "submit", "Submit", show=False), Binding( @@ -769,7 +771,7 @@ def action_home(self, select: bool = False) -> None: select: If `True`, select the text between the old and new cursor positions. """ if select: - self.selection = Selection(0, self.cursor_position) + self.selection = Selection(self.cursor_position, 0) else: self.cursor_position = 0 From 9212b80b2b9103276d74923e2e26e54bd8a9e678 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 15:36:53 +0000 Subject: [PATCH 22/34] Testing Input.replace and Input.delete --- src/textual/widgets/input.py | 3 ++ tests/input/test_input_replace.py | 62 +++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 tests/input/test_input_replace.py diff --git a/src/textual/widgets/input.py b/src/textual/widgets/input.py index e69de29bb2..9d2e19975a 100644 --- a/src/textual/widgets/input.py +++ b/src/textual/widgets/input.py @@ -0,0 +1,3 @@ +from textual.widgets._input import Selection + +__all__ = ["Selection"] diff --git a/tests/input/test_input_replace.py b/tests/input/test_input_replace.py new file mode 100644 index 0000000000..38011ad82c --- /dev/null +++ b/tests/input/test_input_replace.py @@ -0,0 +1,62 @@ +import pytest + +from textual.app import App, ComposeResult +from textual.widgets import Input + +_FORWARD_DELETIONS = [ + ((0, 0), "0123456789"), # Delete nothing + ((0, 10), ""), # Delete all + ((0, 1), "123456789"), # Delete first character + ((5, 10), "01234"), # Delete last 5 characters + ((3, 6), "0126789"), # Delete middle 3 characters + ((5, 20), "01234"), # Delete past end results in clamp +] +_REVERSE_DELETIONS = [ + ((end, start), value_after) for (start, end), value_after in _FORWARD_DELETIONS +] +DELETIONS = _FORWARD_DELETIONS + _REVERSE_DELETIONS +"""The same deletions performed in both forward and reverse directions.""" + + +@pytest.mark.parametrize("selection,value_after", DELETIONS) +async def test_input_delete(selection: tuple[int, int], value_after: str): + class InputApp(App[None]): + TEST_TEXT = "0123456789" + + def compose(self) -> ComposeResult: + yield Input(self.TEST_TEXT) + + async with InputApp().run_test() as pilot: + app = pilot.app + input = app.query_one(Input) + input.delete(*selection) + assert input.value == value_after + + +_FORWARD_REPLACEMENTS = [ + ((0, 0), "", "0123456789"), + ((0, 1), "X", "X123456789"), # Replace first character with "X" + ((5, 10), "X", "01234X"), # Replace last 5 characters with "X" + ((3, 6), "X", "012X6789"), # Replace middle 3 characters with "X" + ((5, 20), "X", "01234X"), # Replace past end results in clamp +] +_REVERSE_REPLACEMENTS = [ + ((end, start), replacement, result) + for (start, end), replacement, result in _FORWARD_REPLACEMENTS +] +REPLACEMENTS = _FORWARD_REPLACEMENTS + _REVERSE_REPLACEMENTS + + +@pytest.mark.parametrize("selection,replacement,result", REPLACEMENTS) +async def test_input_replace(selection: tuple[int, int], replacement: str, result: str): + class InputApp(App[None]): + TEST_TEXT = "0123456789" + + def compose(self) -> ComposeResult: + yield Input(self.TEST_TEXT) + + async with InputApp().run_test() as pilot: + app = pilot.app + input = app.query_one(Input) + input.replace(replacement, *selection) + assert input.value == result From c1d871d0c49337440e619e5a5753a1f8cd289aec Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 15:43:48 +0000 Subject: [PATCH 23/34] Create new snapshot --- .../test_snapshots/test_help_panel.svg | 146 +++++++++--------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg index ce0c8ee333..5073a4d505 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg @@ -19,161 +19,161 @@ font-weight: 700; } - .terminal-3760089357-matrix { + .terminal-1412389280-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3760089357-title { + .terminal-1412389280-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3760089357-r1 { fill: #121212 } -.terminal-3760089357-r2 { fill: #0178d4 } -.terminal-3760089357-r3 { fill: #4f4f4f } -.terminal-3760089357-r4 { fill: #c5c8c6 } -.terminal-3760089357-r5 { fill: #fea62b;font-weight: bold } -.terminal-3760089357-r6 { fill: #e0e0e0 } -.terminal-3760089357-r7 { fill: #000000 } + .terminal-1412389280-r1 { fill: #121212 } +.terminal-1412389280-r2 { fill: #0178d4 } +.terminal-1412389280-r3 { fill: #4f4f4f } +.terminal-1412389280-r4 { fill: #c5c8c6 } +.terminal-1412389280-r5 { fill: #fea62b;font-weight: bold } +.terminal-1412389280-r6 { fill: #e0e0e0 } +.terminal-1412389280-r7 { fill: #000000 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - HelpPanelApp + HelpPanelApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -        ↑Scroll Up        -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁        ↓Scroll Down      -        ←Move cursor left -        →Move cursor      -right            -  home ^aGo to start      -   end ^eGo to end        -     pgupPage Up          -     pgdnPage Down        -    ^pgupPage Left        -    ^pgdnPage Right       -  shift+←Move cursor left -and select       -       ^←Move cursor left -a word           - shift+^←Move cursor left -a word and       -select           -  shift+→Move cursor     ▆▆ -right and select -       ^→Move cursor      -right a word     - shift+^→Move cursor      -right a word and -select           -        ⌫Delete character -left             -   del ^dDelete character -right            + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +         ↑Scroll Up       +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁         ↓Scroll Down     +         ←Move cursor     +left            +         →Move cursor     +right           +   home ^aGo to start     +    end ^eGo to end       +      pgupPage Up         +      pgdnPage Down       +     ^pgupPage Left       +     ^pgdnPage Right      +   shift+←Move cursor     +left and select +        ^←Move cursor     +left a word     +  shift+^←Move cursor    ▅▅ +left a word and +select          +   shift+→Move cursor     +right and       +select          +        ^→Move cursor     +right a word    +  shift+^→Move cursor     +right a word    +and select      +         ⌫Delete          +character left  From 4972463743dbdc01ce708e19f625a846432e6889 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 15:46:18 +0000 Subject: [PATCH 24/34] Import annotations from __future__ --- tests/input/test_input_replace.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/input/test_input_replace.py b/tests/input/test_input_replace.py index 38011ad82c..40276ffab1 100644 --- a/tests/input/test_input_replace.py +++ b/tests/input/test_input_replace.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from textual.app import App, ComposeResult From 3336181cebab1139572a3d527cd46f7062c1af17 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 16:21:53 +0000 Subject: [PATCH 25/34] Selection goes under highlights --- src/textual/widgets/_input.py | 2 +- tests/input/test_input.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/input/test_input.py diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index d0f4b277a5..6b418c6002 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -565,7 +565,7 @@ def render_line(self, y: int) -> Strip: start, end = self.selection start, end = sorted((start, end)) selection_style = self.get_component_rich_style("input--selection") - result.stylize(selection_style, start, end) + result.stylize_before(selection_style, start, end) if self._cursor_visible: cursor_style = self.get_component_rich_style("input--cursor") diff --git a/tests/input/test_input.py b/tests/input/test_input.py new file mode 100644 index 0000000000..e69de29bb2 From dbc0dac39e232d27aa7d7ef2403ed505720d9af0 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 16:23:32 +0000 Subject: [PATCH 26/34] Rename `empty` to `is_empty` in Input Selection class --- src/textual/widgets/_input.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 6b418c6002..8f26613f18 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -61,7 +61,7 @@ def cursor(cls, cursor_position: int) -> Selection: return cls(cursor_position, cursor_position) @property - def empty(self) -> bool: + def is_empty(self) -> bool: return self.start == self.end @@ -561,7 +561,7 @@ def render_line(self, y: int) -> Strip: ) if self.has_focus: - if not self.selection.empty: + if not self.selection.is_empty: start, end = self.selection start, end = sorted((start, end)) selection_style = self.get_component_rich_style("input--selection") @@ -640,7 +640,7 @@ async def _on_key(self, event: events.Key) -> None: event.stop() assert event.character is not None selection = self.selection - if selection.empty: + if selection.is_empty: self.insert_text_at_cursor(event.character) else: self.replace(event.character, *selection) @@ -650,7 +650,7 @@ def _on_paste(self, event: events.Paste) -> None: if event.text: line = event.text.splitlines()[0] selection = self.selection - if selection.empty: + if selection.is_empty: self.insert_text_at_cursor(line) else: self.replace(line, *selection) @@ -900,14 +900,14 @@ def delete_selection(self) -> None: def action_delete_right(self) -> None: """Delete one character at the current cursor position.""" - if self.selection.empty: + if self.selection.is_empty: self.delete(self.cursor_position, self.cursor_position + 1) else: self.delete_selection() def action_delete_right_word(self) -> None: """Delete the current character and all rightward to the start of the next word.""" - if not self.selection.empty: + if not self.selection.is_empty: self.delete_selection() return @@ -927,21 +927,21 @@ def action_delete_right_word(self) -> None: def action_delete_right_all(self) -> None: """Delete the current character and all characters to the right of the cursor position.""" - if self.selection.empty: + if self.selection.is_empty: self.delete(self.cursor_position, len(self.value)) else: self.delete_selection() def action_delete_left(self) -> None: """Delete one character to the left of the current cursor position.""" - if self.selection.empty: + if self.selection.is_empty: self.delete(self.cursor_position - 1, self.cursor_position) else: self.delete_selection() def action_delete_left_word(self) -> None: """Delete leftward of the cursor position to the start of a word.""" - if not self.selection.empty: + if not self.selection.is_empty: self.delete_selection() return @@ -963,7 +963,7 @@ def action_delete_left_word(self) -> None: def action_delete_left_all(self) -> None: """Delete all characters to the left of the cursor position.""" - if self.selection.empty: + if self.selection.is_empty: self.delete(0, self.cursor_position) else: self.delete_selection() From c33f36e7fc6d2ca2bb1994d6c2471b77badf67cc Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 16:26:29 +0000 Subject: [PATCH 27/34] Select Input text on focus --- src/textual/widgets/_input.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 8f26613f18..529a336fa4 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -292,6 +292,7 @@ def __init__( validators: Validator | Iterable[Validator] | None = None, validate_on: Iterable[InputValidationOn] | None = None, valid_empty: bool = False, + select_on_focus: bool = True, name: str | None = None, id: str | None = None, classes: str | None = None, @@ -315,6 +316,7 @@ def __init__( which determine when to do input validation. The default is to do validation for all messages. valid_empty: Empty values are valid. + select_on_focus: Whether to select all text on focus. name: Optional name for the input widget. id: Optional ID for the widget. classes: Optional initial classes for the widget. @@ -385,6 +387,8 @@ def __init__( if tooltip is not None: self.tooltip = tooltip + self.select_on_focus = select_on_focus + def _position_to_cell(self, position: int) -> int: """Convert an index within the value to cell position. @@ -630,6 +634,8 @@ def _on_blur(self, event: Blur) -> None: def _on_focus(self, event: Focus) -> None: self._restart_blink() + if self.select_on_focus: + self.selection = Selection(0, len(self.value)) self.app.cursor_position = self.cursor_screen_offset self._suggestion = "" From 3d21493d5b5d94a4a40d26f9c2a381050ca37e5e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 16:36:00 +0000 Subject: [PATCH 28/34] Snapshot fixes --- src/textual/widgets/_masked_input.py | 6 +- .../test_input_setting_value.svg | 118 +++++++++--------- .../snapshot_apps/input_suggestions.py | 2 +- tests/snapshot_tests/test_snapshots.py | 2 +- tests/test_masked_input.py | 4 +- 5 files changed, 68 insertions(+), 64 deletions(-) diff --git a/src/textual/widgets/_masked_input.py b/src/textual/widgets/_masked_input.py index ed6aef7527..560258f332 100644 --- a/src/textual/widgets/_masked_input.py +++ b/src/textual/widgets/_masked_input.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: pass -from textual.reactive import var +from textual.reactive import Reactive, var from textual.validation import ValidationResult, Validator from textual.widgets._input import Input @@ -443,7 +443,7 @@ def empty_mask(self) -> str: class MaskedInput(Input, can_focus=True): """A masked text input widget.""" - template = var("") + template: Reactive[str] = var("") """Input template mask currently in use.""" def __init__( @@ -455,6 +455,7 @@ def __init__( validators: Validator | Iterable[Validator] | None = None, validate_on: Iterable[InputValidationOn] | None = None, valid_empty: bool = False, + select_on_focus: bool = True, name: str | None = None, id: str | None = None, classes: str | None = None, @@ -484,6 +485,7 @@ def __init__( validators=validators, validate_on=validate_on, valid_empty=valid_empty, + select_on_focus=select_on_focus, name=name, id=id, classes=classes, diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_setting_value.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_setting_value.svg index 440d5dba1d..f9bf6c327d 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_setting_value.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_setting_value.svg @@ -19,135 +19,135 @@ font-weight: 700; } - .terminal-855704843-matrix { + .terminal-3699770774-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-855704843-title { + .terminal-3699770774-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-855704843-r1 { fill: #121212 } -.terminal-855704843-r2 { fill: #0178d4 } -.terminal-855704843-r3 { fill: #c5c8c6 } -.terminal-855704843-r4 { fill: #e0e0e0 } -.terminal-855704843-r5 { fill: #191919 } -.terminal-855704843-r6 { fill: #737373 } + .terminal-3699770774-r1 { fill: #121212 } +.terminal-3699770774-r2 { fill: #0178d4 } +.terminal-3699770774-r3 { fill: #c5c8c6 } +.terminal-3699770774-r4 { fill: #e0e0e0 } +.terminal-3699770774-r5 { fill: #191919 } +.terminal-3699770774-r6 { fill: #737373 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - InputApp + InputApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -default -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -set attribute in compose                                                   -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Placeholder, no value -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -set in on_mount                                                            -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +default +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +set attribute in compose                                                   +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Placeholder, no value +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +set in on_mount                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + diff --git a/tests/snapshot_tests/snapshot_apps/input_suggestions.py b/tests/snapshot_tests/snapshot_apps/input_suggestions.py index 9955fa4175..f6d16ca9e6 100644 --- a/tests/snapshot_tests/snapshot_apps/input_suggestions.py +++ b/tests/snapshot_tests/snapshot_apps/input_suggestions.py @@ -15,7 +15,7 @@ class FruitsApp(App[None]): """ def compose(self) -> ComposeResult: - yield Input("straw", suggester=SuggestFromList(fruits)) + yield Input("straw", suggester=SuggestFromList(fruits), select_on_focus=False) yield Input("straw", suggester=SuggestFromList(fruits)) yield Input("p", suggester=SuggestFromList(fruits)) yield Input("b", suggester=SuggestFromList(fruits)) diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 0944845991..b7e9a59a86 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -176,7 +176,7 @@ def test_input_cursor(snap_compare): class InputApp(App[None]): def compose(self) -> ComposeResult: yield Input(value="こんにちは") - input = Input(value="こんにちは") + input = Input(value="こんにちは", select_on_focus=False) input.focus() input.action_cursor_left() yield input diff --git a/tests/test_masked_input.py b/tests/test_masked_input.py index 698d7f11c5..72b4d9dc09 100644 --- a/tests/test_masked_input.py +++ b/tests/test_masked_input.py @@ -18,7 +18,9 @@ def __init__(self, template: str, placeholder: str = ""): self.placeholder = placeholder def compose(self) -> ComposeResult: - yield MaskedInput(template=self.template, placeholder=self.placeholder) + yield MaskedInput( + template=self.template, placeholder=self.placeholder, select_on_focus=False + ) @on(MaskedInput.Changed) @on(MaskedInput.Submitted) From 083b69e1bff50bc09e44f3f5ee1cbb9c04dd75d8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 16:49:06 +0000 Subject: [PATCH 29/34] Ensure we dont scroll to the end of an input on the initial value set --- src/textual/widgets/_input.py | 11 +- .../test_snapshots/test_app_blur.svg | 112 +++++++++--------- .../test_input_initial_scroll.svg | 76 ++++++++++++ tests/snapshot_tests/test_snapshots.py | 13 ++ 4 files changed, 151 insertions(+), 61 deletions(-) create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_input_initial_scroll.svg diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 529a336fa4..026e79bafd 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -434,11 +434,12 @@ def validate_selection(self, selection: Selection) -> Selection: def _watch_selection(self, selection: Selection) -> None: self.app.cursor_position = self.cursor_screen_offset - self.scroll_to_region( - Region(self._cursor_offset, 0, width=1, height=1), - force=True, - animate=False, - ) + if not self._initial_value: + self.scroll_to_region( + Region(self._cursor_offset, 0, width=1, height=1), + force=True, + animate=False, + ) def _watch_cursor_blink(self, blink: bool) -> None: """Ensure we handle updating the cursor blink at runtime.""" diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_blur.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_blur.svg index 186c3161ee..800319e269 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_blur.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_blur.svg @@ -19,133 +19,133 @@ font-weight: 700; } - .terminal-141898065-matrix { + .terminal-3758502277-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-141898065-title { + .terminal-3758502277-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-141898065-r1 { fill: #e0e0e0 } -.terminal-141898065-r2 { fill: #c5c8c6 } -.terminal-141898065-r3 { fill: #121212 } -.terminal-141898065-r4 { fill: #191919 } + .terminal-3758502277-r1 { fill: #e0e0e0 } +.terminal-3758502277-r2 { fill: #c5c8c6 } +.terminal-3758502277-r3 { fill: #121212 } +.terminal-3758502277-r4 { fill: #191919 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AppBlurApp + AppBlurApp - + - - - - - - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -This should be the blur style      -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -his should also be the blur style  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +This should be the blur style      +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +This should also be the blur style +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_initial_scroll.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_initial_scroll.svg new file mode 100644 index 0000000000..e7fb73aa58 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_input_initial_scroll.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + InputInitialScrollApp + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +the quick brow +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index b7e9a59a86..99d2e2523b 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -208,6 +208,19 @@ def compose(self) -> ComposeResult: ) +def test_input_initial_scroll(snap_compare): + """When the input is smaller than its content, the start of the content should + be visible, not the end.""" + + class InputInitialScrollApp(App[None]): + AUTO_FOCUS = None + + def compose(self) -> ComposeResult: + yield Input(value="the quick brown fox jumps over the lazy dog") + + assert snap_compare(InputInitialScrollApp(), terminal_size=(20, 5)) + + def test_input_selection(snap_compare): """BCDEF should be visible, and DEF should be selected. The cursor should be sitting above 'D'.""" From 8e75f4890eb96c71e1353b6f3a1a9fa44d2d71ed Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 17:04:32 +0000 Subject: [PATCH 30/34] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7807c1fbe0..1780a932e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added "tab" border style https://github.com/Textualize/textual/pull/5335 - Added support for XML syntax highlighting https://github.com/Textualize/textual/pull/5320 - Added `TextArea.update_highlight_query` https://github.com/Textualize/textual/pull/5320 +- `Input` widget now supports text selection via mouse and keyboard https://github.com/Textualize/textual/pull/5340 +- Added new keybinds (hold shift) for text selection in `Input` https://github.com/Textualize/textual/pull/5340 +- Added `Input.selection` reactive attribute for reading and updating the current selection https://github.com/Textualize/textual/pull/5340 +- Added `Input.select_on_focus` (default `True`) to enable/disable selecting all text in an `Input` on focus https://github.com/Textualize/textual/pull/5340 +- `Input` can now be scrolled independently of cursor position (hold shift and scroll with the mouse wheel in supported environments) https://github.com/Textualize/textual/pull/5340 + +## Changed + +- Breaking change: Removed `Input` reactive attributes `view_position`, `cursor_position` (now exists as a property which proxies to the `Input.selection` reactive attribute), https://github.com/Textualize/textual/pull/5340 +- `Input.restrict` now checked on all edit operations (rather than just on `insert`) https://github.com/Textualize/textual/pull/5340 ## Fixed From 9ff7cbcd92c11bee0fe607c217b553432bce7fc3 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 17:30:30 +0000 Subject: [PATCH 31/34] Extra Input selection tests --- CHANGELOG.md | 2 ++ src/textual/widgets/_input.py | 6 ++++++ tests/input/test_input.py | 0 tests/input/test_input_properties.py | 32 ++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+) delete mode 100644 tests/input/test_input.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1780a932e1..ed140e3f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added new keybinds (hold shift) for text selection in `Input` https://github.com/Textualize/textual/pull/5340 - Added `Input.selection` reactive attribute for reading and updating the current selection https://github.com/Textualize/textual/pull/5340 - Added `Input.select_on_focus` (default `True`) to enable/disable selecting all text in an `Input` on focus https://github.com/Textualize/textual/pull/5340 +- Added methods `Input.replace`, `Input.insert`, `Input.delete`, `Input.delete_selection` for editing text https://github.com/Textualize/textual/pull/5340 +- Added `Input.selected_text` property for getting the currently selected text https://github.com/Textualize/textual/pull/5340 - `Input` can now be scrolled independently of cursor position (hold shift and scroll with the mouse wheel in supported environments) https://github.com/Textualize/textual/pull/5340 ## Changed diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 026e79bafd..5665591975 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -743,6 +743,12 @@ def clear(self) -> None: """Clear the input.""" self.value = "" + @property + def selected_text(self) -> str: + """The text between the start and end points of the current selection.""" + start, end = sorted(self.selection) + return self.value[start:end] + def action_cursor_left(self, select: bool = False) -> None: """Move the cursor one position to the left. diff --git a/tests/input/test_input.py b/tests/input/test_input.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/input/test_input_properties.py b/tests/input/test_input_properties.py index 586989ceac..ef518ba69d 100644 --- a/tests/input/test_input_properties.py +++ b/tests/input/test_input_properties.py @@ -6,6 +6,7 @@ from textual.app import App, ComposeResult from textual.containers import Container from textual.widgets import Input +from textual.widgets.input import Selection class InputApp(App[None]): @@ -61,3 +62,34 @@ async def test_input_height(): input_widget.styles.height.value == input_widget.parent.styles.height.value ) assert input_widget.parent.styles.height.value == 1 + + +async def test_input_selected_text(): + async with InputApp().run_test() as pilot: + input_widget = pilot.app.query_one(Input) + input_widget.value = "Hello, world!" + input_widget.selection = Selection(0, 4) + assert input_widget.selected_text == "Hell" + + # Reverse selection + input_widget.selection = Selection(4, 0) + assert input_widget.selected_text == "Hell" + + # Empty selection + input_widget.selection = Selection(4, 4) + assert input_widget.selected_text == "" + + +async def test_input_selection_deleted_programmatically(): + async with InputApp().run_test() as pilot: + input_widget = pilot.app.query_one(Input) + input_widget.value = "Hello, world!" + input_widget.selection = Selection(0, 4) + input_widget.delete_selection() + assert input_widget.value == "o, world!" + + # Reverse selection + input_widget.value = "Hello, world!" + input_widget.selection = Selection(4, 0) + input_widget.delete_selection() + assert input_widget.value == "o, world!" From f9c0aebc7a2e7db5e2a271f8982242547f57d02d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 17:33:09 +0000 Subject: [PATCH 32/34] Add copy_selection bindings --- src/textual/widgets/_input.py | 5 +++++ src/textual/widgets/_text_area.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 5665591975..126bf9da1c 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -114,6 +114,7 @@ class Input(ScrollView): "ctrl+f", "delete_right_word", "Delete right to start of word", show=False ), Binding("ctrl+k", "delete_right_all", "Delete all to the right", show=False), + Binding("ctrl+c", "copy_selection", "Copy selected text", show=False), ] """ | Key(s) | Description | @@ -990,3 +991,7 @@ async def action_submit(self) -> None: self.validate(self.value) if "submitted" in self.validate_on else None ) self.post_message(self.Submitted(self, self.value, validation_result)) + + def action_copy_selection(self) -> None: + """Copy the current selection to the clipboard.""" + self.app.copy_to_clipboard(self.selected_text) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index e498210336..2b68efbc60 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -238,6 +238,7 @@ class TextArea(ScrollView): ), Binding("ctrl+z", "undo", "Undo", show=False), Binding("ctrl+y", "redo", "Redo", show=False), + Binding("ctrl+c", "copy_selection", "Copy selected text", show=False), ] """ | Key(s) | Description | @@ -2279,6 +2280,10 @@ def action_delete_word_right(self) -> None: self._delete_via_keyboard(end, to_location) + def action_copy_selection(self) -> None: + """Copy the current selection to the clipboard.""" + self.app.copy_to_clipboard(self.selected_text) + @lru_cache(maxsize=128) def build_byte_to_codepoint_dict(data: bytes) -> dict[int, int]: From 2d773ce3bc87d2db1aee6f77d04c70adcb6c1935 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 17:36:05 +0000 Subject: [PATCH 33/34] Add missing docstrings --- src/textual/widgets/_input.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 126bf9da1c..7a14fc393f 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -62,6 +62,7 @@ def cursor(cls, cursor_position: int) -> Selection: @property def is_empty(self) -> bool: + """Return True if the selection is empty.""" return self.start == self.end @@ -209,10 +210,12 @@ class Input(ScrollView): @property def cursor_position(self) -> int: + """The current position of the cursor, corresponding to the end of the selection.""" return self.selection.end @cursor_position.setter def cursor_position(self, position: int) -> None: + """Set the current position of the cursor.""" self.selection = Selection.cursor(position) selection: Reactive[Selection] = reactive(Selection.cursor(0)) From bffbc57fb9634d4c2074c9cb00b3985f31072796 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 5 Dec 2024 17:37:39 +0000 Subject: [PATCH 34/34] Update snapshots --- .../test_snapshots/test_help_panel.svg | 145 +++++++++--------- 1 file changed, 72 insertions(+), 73 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg index 5073a4d505..ea40e194ef 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg @@ -19,161 +19,160 @@ font-weight: 700; } - .terminal-1412389280-matrix { + .terminal-3974648907-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1412389280-title { + .terminal-3974648907-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1412389280-r1 { fill: #121212 } -.terminal-1412389280-r2 { fill: #0178d4 } -.terminal-1412389280-r3 { fill: #4f4f4f } -.terminal-1412389280-r4 { fill: #c5c8c6 } -.terminal-1412389280-r5 { fill: #fea62b;font-weight: bold } -.terminal-1412389280-r6 { fill: #e0e0e0 } -.terminal-1412389280-r7 { fill: #000000 } + .terminal-3974648907-r1 { fill: #121212 } +.terminal-3974648907-r2 { fill: #0178d4 } +.terminal-3974648907-r3 { fill: #4f4f4f } +.terminal-3974648907-r4 { fill: #c5c8c6 } +.terminal-3974648907-r5 { fill: #fea62b;font-weight: bold } +.terminal-3974648907-r6 { fill: #e0e0e0 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - HelpPanelApp + HelpPanelApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -         ↑Scroll Up       -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁         ↓Scroll Down     -         ←Move cursor     -left            -         →Move cursor     -right           -   home ^aGo to start     -    end ^eGo to end       -      pgupPage Up         -      pgdnPage Down       -     ^pgupPage Left       -     ^pgdnPage Right      -   shift+←Move cursor     -left and select -        ^←Move cursor     -left a word     -  shift+^←Move cursor    ▅▅ -left a word and -select          -   shift+→Move cursor     -right and       -select          -        ^→Move cursor     -right a word    -  shift+^→Move cursor     -right a word    -and select      -         ⌫Delete          -character left  + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +         ↑Scroll Up       +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁         ↓Scroll Down     +         ←Move cursor     +left            +         →Move cursor     +right           +   home ^aGo to start     +    end ^eGo to end       +      pgupPage Up         +      pgdnPage Down       +     ^pgupPage Left       +     ^pgdnPage Right      +   shift+←Move cursor     +left and select +        ^←Move cursor     +left a word     +  shift+^←Move cursor     +left a word and +select          +   shift+→Move cursor     +right and       +select          +        ^→Move cursor     +right a word    +  shift+^→Move cursor     +right a word    +and select      +         ⌫Delete          +character left