mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
TextArea default CSS (#4074)
* Starting CSS work for TextArea * Remove xfail marker from test * Adding component classes to TextArea, not using them yet * Adding docstring for TextArea new component classes * Passing all component styles to the theme so that they may be applied. * Applying cursor component style * Applying text-area--cursor-line component style * Applying text-area--cursor-gutter component style * Applying gutter cursor style correctly * Default cursor styling * CSS theming of the selection style * default matching bracket theme in text area * Support toggling dark and light mode * Improve the theme on light mode for the cursor * null check * Snapshot for new default "css" theme of TextArea * Hide cursor when TextArea doesnt have focus * Some new docs for TextArea * Add border to TextArea to fit more with Input * Add note on how to remove the focus border effect * Updating snapshots * Updating snapshots * Fixing tests to account for new TextArea border * Fix a typo * Updating CHANGELOG * Update docs/widgets/text_area.md Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> * Add missing docstring --------- Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com>
This commit is contained in:
@@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Breaking change: Significant changes to `TextArea.__init__` default values/behaviour https://github.com/Textualize/textual/pull/3933
|
||||
- `soft_wrap=True` - soft wrapping is now enabled by default.
|
||||
- `show_line_numbers=False` - line numbers are now disabled by default.
|
||||
- `tab_behaviour="focus"` - pressing the tab key now switches focus instead of indenting by default.
|
||||
- `tab_behaviour="focus"` - pressing the tab key now switches focus instead of indenting by default.
|
||||
- Breaking change: `TextArea` default theme changed to CSS, and default styling changed https://github.com/Textualize/textual/pull/4074
|
||||
- Breaking change: `DOMNode.has_pseudo_class` now accepts a single name only https://github.com/Textualize/textual/pull/3970
|
||||
- Made `textual.cache` (formerly `textual._cache`) public https://github.com/Textualize/textual/pull/3976
|
||||
- `Tab.label` can now be used to change the label of a tab https://github.com/Textualize/textual/pull/3979
|
||||
@@ -27,6 +28,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Added `TextArea.code_editor` classmethod/alternative constructor https://github.com/Textualize/textual/pull/3933
|
||||
- Added `TextArea.wrapped_document` attribute which can convert between wrapped visual coordinates and locations https://github.com/Textualize/textual/pull/3933
|
||||
- Added `show_line_numbers` to `TextArea.__init__` https://github.com/Textualize/textual/pull/3933
|
||||
- Added component classes allowing `TextArea` to be styled using CSS https://github.com/Textualize/textual/pull/4074
|
||||
- Added `Query.blur` and `Query.focus` https://github.com/Textualize/textual/pull/4012
|
||||
- Added `MessagePump.message_queue_size` https://github.com/Textualize/textual/pull/4012
|
||||
- Added `TabbedContent.active_pane` https://github.com/Textualize/textual/pull/4012
|
||||
|
||||
@@ -173,6 +173,17 @@ There are some methods available which make common selections easier:
|
||||
Themes give you control over the look and feel, including syntax highlighting,
|
||||
the cursor, selection, gutter, and more.
|
||||
|
||||
#### Default theme
|
||||
|
||||
The default `TextArea` theme is called `css`.
|
||||
This a theme which takes values entirely from CSS.
|
||||
This means that the default appearance of the widget fits nicely into a standard Textual application,
|
||||
and looks right on both dark and light mode.
|
||||
|
||||
More complex applications such as code editors will likely want to use pre-defined themes such as `monokai`.
|
||||
This involves using a `TextAreaTheme` object, which we cover in detail below.
|
||||
This allows full customization of the `TextArea`, including syntax highlighting, at the code level.
|
||||
|
||||
#### Using builtin themes
|
||||
|
||||
The initial theme of the `TextArea` is determined by the `theme` parameter.
|
||||
@@ -232,6 +243,7 @@ my_theme = TextAreaTheme(
|
||||
|
||||
Attributes like `cursor_style` and `cursor_line_style` apply general language-agnostic
|
||||
styling to the widget.
|
||||
If you choose not to supply a value for one of these attributes, it will be taken from the CSS component styles.
|
||||
|
||||
The `syntax_styles` attribute of `TextAreaTheme` is used for syntax highlighting and
|
||||
depends on the `language` currently in use.
|
||||
@@ -485,9 +497,13 @@ The `TextArea` widget defines the following bindings:
|
||||
|
||||
## Component classes
|
||||
|
||||
The `TextArea` widget defines no component classes.
|
||||
The `TextArea` defines component classes that can style various aspects of the widget.
|
||||
Styles from the `theme` attribute take priority.
|
||||
|
||||
Styling should be done exclusively via [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme].
|
||||
::: textual.widgets.TextArea.COMPONENT_CLASSES
|
||||
options:
|
||||
show_root_heading: false
|
||||
show_root_toc_entry: false
|
||||
|
||||
## See also
|
||||
|
||||
@@ -499,6 +515,11 @@ Styling should be done exclusively via [`TextAreaTheme`][textual.widgets.text_ar
|
||||
- The tree-sitter Python bindings [repository](https://github.com/tree-sitter/py-tree-sitter).
|
||||
- `py-tree-sitter-languages` [repository](https://github.com/grantjenks/py-tree-sitter-languages) (provides binary wheels for a large variety of tree-sitter languages).
|
||||
|
||||
|
||||
## Additional notes
|
||||
|
||||
- To remove the outline effect when the `TextArea` is focused, you can set `border: none; padding: 0;` in your CSS.
|
||||
|
||||
---
|
||||
|
||||
::: textual.widgets._text_area.TextArea
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from rich.style import Style
|
||||
|
||||
@@ -8,6 +9,9 @@ from textual.app import DEFAULT_COLORS
|
||||
from textual.color import Color
|
||||
from textual.design import DEFAULT_DARK_SURFACE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from textual.widgets import TextArea
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextAreaTheme:
|
||||
@@ -63,10 +67,18 @@ class TextAreaTheme:
|
||||
syntax_styles: dict[str, Style] = field(default_factory=dict)
|
||||
"""The mapping of tree-sitter names from the `highlight_query` to Rich styles."""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Generate some styles if they haven't been supplied."""
|
||||
if self.base_style is None:
|
||||
self.base_style = Style()
|
||||
def apply_css(self, text_area: TextArea) -> None:
|
||||
"""Apply CSS rules from a TextArea to be used for fallback styling.
|
||||
|
||||
If any attributes in the theme aren't supplied, they'll be filled with the appropriate
|
||||
base CSS (e.g. color, background, etc.) and component CSS (e.g. text-area--cursor) from
|
||||
the supplied TextArea.
|
||||
|
||||
Args:
|
||||
text_area: The TextArea instance to retrieve fallback styling from.
|
||||
"""
|
||||
self.base_style = text_area.rich_style or Style()
|
||||
get_style = text_area.get_component_rich_style
|
||||
|
||||
if self.base_style.color is None:
|
||||
self.base_style = Style(color="#f3f3f3", bgcolor=self.base_style.bgcolor)
|
||||
@@ -81,33 +93,58 @@ class TextAreaTheme:
|
||||
assert self.base_style.bgcolor is not None
|
||||
|
||||
if self.gutter_style is None:
|
||||
self.gutter_style = self.base_style.copy()
|
||||
gutter_style = get_style("text-area--gutter")
|
||||
if gutter_style:
|
||||
self.gutter_style = gutter_style
|
||||
else:
|
||||
self.gutter_style = self.base_style.copy()
|
||||
|
||||
background_color = Color.from_rich_color(self.base_style.bgcolor)
|
||||
if self.cursor_style is None:
|
||||
self.cursor_style = Style(
|
||||
color=background_color.rich_color,
|
||||
bgcolor=background_color.inverse.rich_color,
|
||||
)
|
||||
# If the theme doesn't contain a cursor style, fallback to component styles.
|
||||
cursor_style = get_style("text-area--cursor")
|
||||
if cursor_style:
|
||||
self.cursor_style = cursor_style
|
||||
else:
|
||||
# There's no component style either, fallback to a default.
|
||||
self.cursor_style = Style(
|
||||
color=background_color.rich_color,
|
||||
bgcolor=background_color.inverse.rich_color,
|
||||
)
|
||||
|
||||
# Apply fallbacks for the styles of the active line and active line gutter.
|
||||
if self.cursor_line_style is None:
|
||||
self.cursor_line_style = get_style("text-area--cursor-line")
|
||||
|
||||
if self.cursor_line_gutter_style is None:
|
||||
self.cursor_line_gutter_style = get_style("text-area--cursor-gutter")
|
||||
|
||||
if self.cursor_line_gutter_style is None and self.cursor_line_style is not None:
|
||||
self.cursor_line_gutter_style = self.cursor_line_style.copy()
|
||||
|
||||
if self.bracket_matching_style is None:
|
||||
bracket_matching_background = background_color.blend(
|
||||
background_color.inverse, factor=0.05
|
||||
)
|
||||
self.bracket_matching_style = Style(
|
||||
bgcolor=bracket_matching_background.rich_color
|
||||
)
|
||||
matching_bracket_style = get_style("text-area--matching-bracket")
|
||||
if matching_bracket_style:
|
||||
self.bracket_matching_style = matching_bracket_style
|
||||
else:
|
||||
bracket_matching_background = background_color.blend(
|
||||
background_color.inverse, factor=0.05
|
||||
)
|
||||
self.bracket_matching_style = Style(
|
||||
bgcolor=bracket_matching_background.rich_color
|
||||
)
|
||||
|
||||
if self.selection_style is None:
|
||||
selection_background_color = background_color.blend(
|
||||
DEFAULT_COLORS["dark"].primary, factor=0.75
|
||||
)
|
||||
self.selection_style = Style.from_color(
|
||||
bgcolor=selection_background_color.rich_color
|
||||
)
|
||||
selection_style = get_style("text-area--selection")
|
||||
if selection_style:
|
||||
self.selection_style = selection_style
|
||||
else:
|
||||
selection_background_color = background_color.blend(
|
||||
DEFAULT_COLORS["dark"].primary, factor=0.75
|
||||
)
|
||||
self.selection_style = Style.from_color(
|
||||
bgcolor=selection_background_color.rich_color
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_builtin_theme(cls, theme_name: str) -> TextAreaTheme | None:
|
||||
@@ -342,12 +379,15 @@ _GITHUB_LIGHT = TextAreaTheme(
|
||||
},
|
||||
)
|
||||
|
||||
_CSS_THEME = TextAreaTheme(name="css")
|
||||
|
||||
_BUILTIN_THEMES = {
|
||||
"css": _CSS_THEME,
|
||||
"monokai": _MONOKAI,
|
||||
"dracula": _DRACULA,
|
||||
"vscode_dark": _DARK_VS,
|
||||
"github_light": _GITHUB_LIGHT,
|
||||
}
|
||||
|
||||
DEFAULT_THEME = TextAreaTheme.get_builtin_theme("monokai")
|
||||
DEFAULT_THEME = TextAreaTheme.get_builtin_theme("basic")
|
||||
"""The default TextAreaTheme used by Textual."""
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Iterable, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Iterable, Optional, Tuple
|
||||
|
||||
from rich.style import Style
|
||||
from rich.text import Text
|
||||
@@ -87,9 +88,70 @@ class TextArea(ScrollView, can_focus=True):
|
||||
TextArea {
|
||||
width: 1fr;
|
||||
height: 1fr;
|
||||
border: tall $background;
|
||||
padding: 0 1;
|
||||
|
||||
&:focus {
|
||||
border: tall $accent;
|
||||
}
|
||||
}
|
||||
|
||||
.text-area--cursor {
|
||||
color: $text 90%;
|
||||
background: $foreground 90%;
|
||||
}
|
||||
|
||||
TextArea:light .text-area--cursor {
|
||||
color: $text 90%;
|
||||
background: $foreground 70%;
|
||||
}
|
||||
|
||||
.text-area--gutter {
|
||||
color: $text 40%;
|
||||
}
|
||||
|
||||
.text-area--cursor-line {
|
||||
background: $boost;
|
||||
}
|
||||
|
||||
.text-area--cursor-gutter {
|
||||
color: $text 60%;
|
||||
background: $boost;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
.text-area--selection {
|
||||
background: $accent-lighten-1 40%;
|
||||
}
|
||||
|
||||
.text-area--matching-bracket {
|
||||
background: $foreground 30%;
|
||||
}
|
||||
"""
|
||||
|
||||
COMPONENT_CLASSES: ClassVar[set[str]] = {
|
||||
"text-area--cursor",
|
||||
"text-area--gutter",
|
||||
"text-area--cursor-gutter",
|
||||
"text-area--cursor-line",
|
||||
"text-area--selection",
|
||||
"text-area--matching-bracket",
|
||||
}
|
||||
"""
|
||||
`TextArea` offers some component classes which can be used to style aspects of the widget.
|
||||
|
||||
Note that any attributes provided in the chosen `TextAreaTheme` will take priority here.
|
||||
|
||||
| Class | Description |
|
||||
| :- | :- |
|
||||
| `text-area--cursor` | Target the cursor. |
|
||||
| `text-area--gutter` | Target the gutter (line number column). |
|
||||
| `text-area--cursor-gutter` | Target the gutter area of the line the cursor is on. |
|
||||
| `text-area--cursor-line` | Target the line the cursor is on. |
|
||||
| `text-area--selection` | Target the current selection. |
|
||||
| `text-area--matching-bracket` | Target matching brackets. |
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("escape", "screen.focus_next", "Shift Focus", show=False),
|
||||
# Cursor movement
|
||||
@@ -235,7 +297,7 @@ TextArea {
|
||||
soft_wrap: Reactive[bool] = reactive(True, init=False)
|
||||
"""True if text should soft wrap."""
|
||||
|
||||
_cursor_blink_visible: Reactive[bool] = reactive(True, repaint=False, init=False)
|
||||
_cursor_visible: Reactive[bool] = reactive(True, repaint=False, init=False)
|
||||
"""Indicates where the cursor is in the blink cycle. If it's currently
|
||||
not visible due to blinking, this is False."""
|
||||
|
||||
@@ -360,6 +422,9 @@ TextArea {
|
||||
|
||||
self.tab_behaviour = tab_behaviour
|
||||
|
||||
# When `app.dark` is toggled, reset the theme (since it caches values).
|
||||
self.watch(self.app, "dark", self._app_dark_toggled, init=False)
|
||||
|
||||
@classmethod
|
||||
def code_editor(
|
||||
cls,
|
||||
@@ -455,6 +520,10 @@ TextArea {
|
||||
# Add the last line of the node range
|
||||
highlights[node_end_row].append((0, node_end_column, highlight_name))
|
||||
|
||||
def watch_has_focus(self, value: bool) -> None:
|
||||
self._cursor_visible = value
|
||||
super().watch_has_focus(value)
|
||||
|
||||
def _watch_selection(
|
||||
self, previous_selection: Selection, selection: Selection
|
||||
) -> None:
|
||||
@@ -575,7 +644,12 @@ TextArea {
|
||||
def _watch_theme(self, theme: str | None) -> None:
|
||||
"""We set the styles on this widget when the theme changes, to ensure that
|
||||
if padding is applied, the colours match."""
|
||||
self._set_theme(theme)
|
||||
|
||||
def _app_dark_toggled(self):
|
||||
self._set_theme(self._theme.name)
|
||||
|
||||
def _set_theme(self, theme: str | None):
|
||||
theme_object: TextAreaTheme | None
|
||||
if theme is None:
|
||||
# If the theme is None, use the default.
|
||||
@@ -594,7 +668,7 @@ TextArea {
|
||||
f"then switch to that theme by setting the `TextArea.theme` attribute."
|
||||
)
|
||||
|
||||
self._theme = theme_object
|
||||
self._theme = dataclasses.replace(theme_object)
|
||||
if theme_object:
|
||||
base_style = theme_object.base_style
|
||||
if base_style:
|
||||
@@ -855,6 +929,10 @@ TextArea {
|
||||
Returns:
|
||||
A rendered line.
|
||||
"""
|
||||
theme = self._theme
|
||||
if theme:
|
||||
theme.apply_css(self)
|
||||
|
||||
document = self.document
|
||||
wrapped_document = self.wrapped_document
|
||||
scroll_x, scroll_y = self.scroll_offset
|
||||
@@ -879,8 +957,6 @@ TextArea {
|
||||
|
||||
line_index, section_offset = line_info
|
||||
|
||||
theme = self._theme
|
||||
|
||||
# Get the line from the Document.
|
||||
line_string = document.get_line(line_index)
|
||||
line = Text(line_string, end="")
|
||||
@@ -956,7 +1032,7 @@ TextArea {
|
||||
|
||||
if cursor_row == line_index:
|
||||
draw_cursor = not self.cursor_blink or (
|
||||
self.cursor_blink and self._cursor_blink_visible
|
||||
self.cursor_blink and self._cursor_visible
|
||||
)
|
||||
if draw_matched_brackets:
|
||||
matching_bracket_style = theme.bracket_matching_style if theme else None
|
||||
@@ -990,9 +1066,9 @@ TextArea {
|
||||
gutter_width = self.gutter_width
|
||||
if self.show_line_numbers:
|
||||
if cursor_row == line_index:
|
||||
gutter_style = theme.cursor_line_gutter_style if theme else None
|
||||
gutter_style = theme.cursor_line_gutter_style
|
||||
else:
|
||||
gutter_style = theme.gutter_style if theme else None
|
||||
gutter_style = theme.gutter_style
|
||||
|
||||
gutter_width_no_margin = gutter_width - 2
|
||||
gutter_content = str(line_index + 1) if section_offset == 0 else ""
|
||||
@@ -1221,19 +1297,19 @@ TextArea {
|
||||
|
||||
def _toggle_cursor_blink_visible(self) -> None:
|
||||
"""Toggle visibility of the cursor for the purposes of 'cursor blink'."""
|
||||
self._cursor_blink_visible = not self._cursor_blink_visible
|
||||
self._cursor_visible = not self._cursor_visible
|
||||
_, cursor_y = self._cursor_offset
|
||||
self.refresh_lines(cursor_y)
|
||||
|
||||
def _restart_blink(self) -> None:
|
||||
"""Reset the cursor blink timer."""
|
||||
if self.cursor_blink:
|
||||
self._cursor_blink_visible = True
|
||||
self._cursor_visible = True
|
||||
self.blink_timer.reset()
|
||||
|
||||
def _pause_blink(self, visible: bool = True) -> None:
|
||||
"""Pause the cursor blinking but ensure it stays visible."""
|
||||
self._cursor_blink_visible = visible
|
||||
self._cursor_visible = visible
|
||||
self.blink_timer.pause()
|
||||
|
||||
async def _on_mouse_down(self, event: events.MouseDown) -> None:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -811,7 +811,7 @@ def test_text_area_language_rendering(language, snap_compare):
|
||||
assert snap_compare(
|
||||
SNAPSHOT_APPS_DIR / "text_area.py",
|
||||
run_before=setup_language,
|
||||
terminal_size=(80, snippet.count("\n") + 2),
|
||||
terminal_size=(80, snippet.count("\n") + 4),
|
||||
)
|
||||
|
||||
|
||||
@@ -842,7 +842,7 @@ I am the final line."""
|
||||
assert snap_compare(
|
||||
SNAPSHOT_APPS_DIR / "text_area.py",
|
||||
run_before=setup_selection,
|
||||
terminal_size=(30, text.count("\n") + 1),
|
||||
terminal_size=(30, text.count("\n") + 4),
|
||||
)
|
||||
|
||||
|
||||
@@ -872,7 +872,7 @@ def hello(name):
|
||||
assert snap_compare(
|
||||
SNAPSHOT_APPS_DIR / "text_area.py",
|
||||
run_before=setup_theme,
|
||||
terminal_size=(48, text.count("\n") + 2),
|
||||
terminal_size=(48, text.count("\n") + 4),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -307,15 +307,15 @@ async def test_cursor_screen_offset_and_terminal_cursor_position_update():
|
||||
async with app.run_test():
|
||||
text_area = app.query_one(TextArea)
|
||||
|
||||
assert app.cursor_position == (3, 0)
|
||||
assert app.cursor_position == (5, 1)
|
||||
|
||||
text_area.cursor_location = (1, 1)
|
||||
|
||||
assert text_area.cursor_screen_offset == (4, 1)
|
||||
assert text_area.cursor_screen_offset == (6, 2)
|
||||
|
||||
# Also ensure that this update has been reported back to the app
|
||||
# for the benefit of IME/emoji popups.
|
||||
assert app.cursor_position == (4, 1)
|
||||
assert app.cursor_position == (6, 2)
|
||||
|
||||
|
||||
async def test_cursor_screen_offset_and_terminal_cursor_position_scrolling():
|
||||
@@ -327,10 +327,10 @@ async def test_cursor_screen_offset_and_terminal_cursor_position_scrolling():
|
||||
async with app.run_test(size=(80, 2)) as pilot:
|
||||
text_area = app.query_one(TextArea)
|
||||
|
||||
assert app.cursor_position == (3, 0)
|
||||
assert app.cursor_position == (5, 1)
|
||||
|
||||
text_area.cursor_location = (5, 0)
|
||||
await pilot.pause()
|
||||
|
||||
assert text_area.cursor_screen_offset == (3, 1)
|
||||
assert app.cursor_position == (3, 1)
|
||||
assert text_area.cursor_screen_offset == (5, 1)
|
||||
assert app.cursor_position == (5, 1)
|
||||
|
||||
@@ -3,7 +3,7 @@ import pytest
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.geometry import Offset
|
||||
from textual.widgets import TextArea
|
||||
from textual.widgets.text_area import Document, Selection
|
||||
from textual.widgets.text_area import Selection
|
||||
|
||||
TEXT = """I must not fear.
|
||||
Fear is the mind-killer.
|
||||
@@ -25,7 +25,7 @@ async def test_mouse_click():
|
||||
async with app.run_test() as pilot:
|
||||
text_area = app.query_one(TextArea)
|
||||
await pilot.click(TextArea, Offset(x=5, y=2))
|
||||
assert text_area.selection == Selection.cursor((2, 2))
|
||||
assert text_area.selection == Selection.cursor((1, 0))
|
||||
|
||||
|
||||
async def test_mouse_click_clamp_from_right():
|
||||
@@ -44,7 +44,7 @@ async def test_mouse_click_gutter_clamp():
|
||||
async with app.run_test() as pilot:
|
||||
text_area = app.query_one(TextArea)
|
||||
await pilot.click(TextArea, Offset(x=0, y=3))
|
||||
assert text_area.selection == Selection.cursor((3, 0))
|
||||
assert text_area.selection == Selection.cursor((2, 0))
|
||||
|
||||
|
||||
async def test_cursor_movement_basic():
|
||||
@@ -260,7 +260,10 @@ async def test_cursor_page_down():
|
||||
text_area.load_text("XXX\n" * 200)
|
||||
text_area.selection = Selection.cursor((0, 1))
|
||||
await pilot.press("pagedown")
|
||||
assert text_area.selection == Selection.cursor((app.console.height - 1, 1))
|
||||
margin = 2
|
||||
assert text_area.selection == Selection.cursor(
|
||||
(app.console.height - 1 - margin, 1)
|
||||
)
|
||||
|
||||
|
||||
async def test_cursor_page_up():
|
||||
@@ -271,19 +274,19 @@ async def test_cursor_page_up():
|
||||
text_area.load_text("XXX\n" * 200)
|
||||
text_area.selection = Selection.cursor((100, 1))
|
||||
await pilot.press("pageup")
|
||||
margin = 2
|
||||
assert text_area.selection == Selection.cursor(
|
||||
(100 - app.console.height + 1, 1)
|
||||
(100 - app.console.height + 1 + margin, 1)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="still to integrate navigator")
|
||||
async def test_cursor_vertical_movement_visual_alignment_snapping():
|
||||
"""When you move the cursor vertically, it should stay vertically
|
||||
aligned even when double-width characters are used."""
|
||||
app = TextAreaApp()
|
||||
async with app.run_test() as pilot:
|
||||
text_area = app.query_one(TextArea)
|
||||
text_area.load_document(Document("こんにちは\n012345"))
|
||||
text_area.text = "こんにちは\n012345"
|
||||
text_area.move_cursor((1, 3), record_width=True)
|
||||
|
||||
# The '3' is aligned with ん at (0, 1)
|
||||
|
||||
Reference in New Issue
Block a user