Skip to content

Commit 4dec4b3

Browse files
committed
Merge branch 'main' of github.com:Textualize/textual into docs-updates-11sep24
2 parents fde2373 + d11ff24 commit 4dec4b3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+3110
-573
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,27 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
99

1010
### Added
1111

12+
- Added `MaskedInput` widget https://github.com/Textualize/textual/pull/4783
1213
- Input validation for floats and integers accept embedded underscores, e.g., "1_234_567" is valid. https://github.com/Textualize/textual/pull/4784
14+
- Support for `"none"` value added to `dock`, `hatch` and `split` styles https://github.com/Textualize/textual/pull/4982
15+
- Support for `"none"` added to box and border style properties (e.g `widget.style.border = "none"`) https://github.com/Textualize/textual/pull/4982
16+
- Docstrings added to most style properties https://github.com/Textualize/textual/pull/4982
1317

1418
### Changed
1519

1620
- Input validation for integers no longer accepts scientific notation like '1.5e2'; must be castable to int. https://github.com/Textualize/textual/pull/4784
21+
- Default `scrollbar-size-vertical` changed to `2` in inline styles to match Widget default CSS (unlikely to affect users) https://github.com/Textualize/textual/pull/4982
22+
- Removed border-right from `Toast` https://github.com/Textualize/textual/pull/4984
23+
- Some fixes in `RichLog` result in slightly different semantics, see docstrings for details https://github.com/Textualize/textual/pull/4978
1724

1825
### Fixed
1926

2027
- Input validation of floats no longer accepts NaN (not a number). https://github.com/Textualize/textual/pull/4784
2128
- Fixed issues with screenshots by simplifying segments only for snapshot tests https://github.com/Textualize/textual/issues/4929
29+
- Fixed `RichLog.write` not respecting `width` parameter https://github.com/Textualize/textual/pull/4978
30+
- Fixed `RichLog` writing at wrong width when `write` occurs before width is known (e.g. in `compose` or `on_mount`) https://github.com/Textualize/textual/pull/4978
31+
- Fixed `RichLog.write` incorrectly shrinking width to `RichLog.min_width` when `shrink=True` (now shrinks to fit content area instead) https://github.com/Textualize/textual/pull/4978
32+
- Fixed flicker when setting `dark` reactive on startup https://github.com/Textualize/textual/pull/4989
2233

2334
## [0.79.1] - 2024-08-31
2435

@@ -107,6 +118,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
107118

108119
- Fixed issue with Enter events causing unresponsive UI https://github.com/Textualize/textual/pull/4833
109120

121+
110122
## [0.75.0] - 2024-08-01
111123

112124
### Added
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from textual.app import App, ComposeResult
2+
from textual.widgets import Label, MaskedInput
3+
4+
5+
class MaskedInputApp(App):
6+
# (1)!
7+
CSS = """
8+
MaskedInput.-valid {
9+
border: tall $success 60%;
10+
}
11+
MaskedInput.-valid:focus {
12+
border: tall $success;
13+
}
14+
MaskedInput {
15+
margin: 1 1;
16+
}
17+
Label {
18+
margin: 1 2;
19+
}
20+
"""
21+
22+
def compose(self) -> ComposeResult:
23+
yield Label("Enter a valid credit card number.")
24+
yield MaskedInput(
25+
template="9999-9999-9999-9999;0", # (2)!
26+
)
27+
28+
29+
app = MaskedInputApp()
30+
31+
if __name__ == "__main__":
32+
app.run()

docs/widget_gallery.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,16 @@ Display a markdown document.
168168
```{.textual path="docs/examples/widgets/markdown.py"}
169169
```
170170

171+
## MaskedInput
172+
173+
A control to enter input according to a template mask.
174+
175+
[MaskedInput reference](./widgets/masked_input.md){ .md-button .md-button--primary }
176+
177+
178+
```{.textual path="docs/examples/widgets/masked_input.py"}
179+
```
180+
171181
## OptionList
172182

173183
Display a vertical list of options (options may be Rich renderables).

docs/widgets/masked_input.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# MaskedInput
2+
3+
!!! tip "Added in version 0.80.0"
4+
5+
A masked input derived from `Input`, allowing to restrict user input and give visual aid via a simple template mask, which also acts as an implicit *[validator][textual.validation.Validator]*.
6+
7+
- [x] Focusable
8+
- [ ] Container
9+
10+
## Example
11+
12+
The example below shows a masked input to ease entering a credit card number.
13+
14+
=== "Output"
15+
16+
```{.textual path="docs/examples/widgets/masked_input.py"}
17+
```
18+
19+
=== "checkbox.py"
20+
21+
```python
22+
--8<-- "docs/examples/widgets/masked_input.py"
23+
```
24+
25+
## Reactive Attributes
26+
27+
| Name | Type | Default | Description |
28+
| ---------- | ----- | ------- | ------------------------- |
29+
| `template` | `str` | `""` | The template mask string. |
30+
31+
### The template string format
32+
33+
A `MaskedInput` template length defines the maximum length of the input value. Each character of the mask defines a regular expression used to restrict what the user can insert in the corresponding position, and whether the presence of the character in the user input is required for the `MaskedInput` value to be considered valid, according to the following table:
34+
35+
| Mask character | Regular expression | Required? |
36+
| -------------- | ------------------ | --------- |
37+
| `A` | `[A-Za-z]` | Yes |
38+
| `a` | `[A-Za-z]` | No |
39+
| `N` | `[A-Za-z0-9]` | Yes |
40+
| `n` | `[A-Za-z0-9]` | No |
41+
| `X` | `[^ ]` | Yes |
42+
| `x` | `[^ ]` | No |
43+
| `9` | `[0-9]` | Yes |
44+
| `0` | `[0-9]` | No |
45+
| `D` | `[1-9]` | Yes |
46+
| `d` | `[1-9]` | No |
47+
| `#` | `[0-9+\-]` | No |
48+
| `H` | `[A-Fa-f0-9]` | Yes |
49+
| `h` | `[A-Fa-f0-9]` | No |
50+
| `B` | `[0-1]` | Yes |
51+
| `b` | `[0-1]` | No |
52+
53+
There are some special characters that can be used to control automatic case conversion during user input: `>` converts all subsequent user input to uppercase; `<` to lowercase; `!` disables automatic case conversion. Any other character that appears in the template mask is assumed to be a separator, which is a character that is automatically inserted when user reaches its position. All mask characters can be escaped by placing `\` in front of them, allowing any character to be used as separator.
54+
The mask can be terminated by `;c`, where `c` is any character you want to be used as placeholder character. The `placeholder` parameter inherited by `Input` can be used to override this allowing finer grain tuning of the placeholder string.
55+
56+
## Messages
57+
58+
- [MaskedInput.Changed][textual.widgets.MaskedInput.Changed]
59+
- [MaskedInput.Submitted][textual.widgets.MaskedInput.Submitted]
60+
61+
## Bindings
62+
63+
The masked input widget defines the following bindings:
64+
65+
::: textual.widgets.MaskedInput.BINDINGS
66+
options:
67+
show_root_heading: false
68+
show_root_toc_entry: false
69+
70+
## Component Classes
71+
72+
The masked input widget provides the following component classes:
73+
74+
::: textual.widgets.MaskedInput.COMPONENT_CLASSES
75+
options:
76+
show_root_heading: false
77+
show_root_toc_entry: false
78+
79+
---
80+
81+
82+
::: textual.widgets.MaskedInput
83+
options:
84+
heading_level: 2

mkdocs-nav.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ nav:
157157
- "widgets/log.md"
158158
- "widgets/markdown_viewer.md"
159159
- "widgets/markdown.md"
160+
- "widgets/masked_input.md"
160161
- "widgets/option_list.md"
161162
- "widgets/placeholder.md"
162163
- "widgets/pretty.md"

src/textual/_arrange.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
TOP_Z = 2**31 - 1
1717

1818

19-
def _build_dock_layers(widgets: Iterable[Widget]) -> Mapping[str, Sequence[Widget]]:
19+
def _build_layers(widgets: Iterable[Widget]) -> Mapping[str, Sequence[Widget]]:
2020
"""Organize widgets into layers.
2121
2222
Args:
@@ -47,17 +47,19 @@ def arrange(
4747

4848
placements: list[WidgetPlacement] = []
4949
scroll_spacing = Spacing()
50-
get_dock = attrgetter("styles.dock")
51-
get_split = attrgetter("styles.split")
50+
51+
get_dock = attrgetter("styles.is_docked")
52+
get_split = attrgetter("styles.is_split")
53+
5254
styles = widget.styles
5355

5456
# Widgets which will be displayed
5557
display_widgets = [child for child in children if child.styles.display != "none"]
5658

5759
# Widgets organized into layers
58-
dock_layers = _build_dock_layers(display_widgets)
60+
layers = _build_layers(display_widgets)
5961

60-
for widgets in dock_layers.values():
62+
for widgets in layers.values():
6163
# Partition widgets in to split widgets and non-split widgets
6264
non_split_widgets, split_widgets = partition(get_split, widgets)
6365
if split_widgets:
@@ -162,7 +164,7 @@ def _arrange_dock_widgets(
162164
right = max(right, widget_width)
163165
else:
164166
# Should not occur, mainly to keep Mypy happy
165-
raise AssertionError("invalid value for edge") # pragma: no-cover
167+
raise AssertionError("invalid value for dock edge") # pragma: no-cover
166168

167169
align_offset = dock_widget.styles._align_size(
168170
(widget_width, widget_height), size
@@ -220,6 +222,9 @@ def _arrange_split_widgets(
220222
elif split == "right":
221223
widget_width = int(widget_width_fraction) + margin.width
222224
view_region, split_region = view_region.split_vertical(-widget_width)
225+
else:
226+
raise AssertionError("invalid value for split edge") # pragma: no-cover
227+
223228
append_placement(
224229
_WidgetPlacement(split_region, null_spacing, split_widget, 1, True)
225230
)

src/textual/_styles_cache.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def render_line(
313313

314314
def line_post(segments: Iterable[Segment]) -> Iterable[Segment]:
315315
"""Apply effects to segments inside the border."""
316-
if styles.has_rule("hatch"):
316+
if styles.has_rule("hatch") and styles.hatch != "none":
317317
character, color = styles.hatch
318318
if character != " " and color.a > 0:
319319
hatch_style = Style.from_color(

src/textual/app.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,8 @@ class App(Generic[ReturnType], DOMNode):
312312
App {
313313
background: $background;
314314
color: $text;
315+
316+
/* When a widget is maximized */
315317
Screen.-maximized-view {
316318
layout: vertical !important;
317319
hatch: right $panel;
@@ -321,6 +323,10 @@ class App(Generic[ReturnType], DOMNode):
321323
dock: initial !important;
322324
}
323325
}
326+
/* Fade the header title when app is blurred */
327+
&:blur HeaderTitle {
328+
text-opacity: 50%;
329+
}
324330
}
325331
*:disabled:can-focus {
326332
opacity: 0.7;
@@ -820,6 +826,17 @@ def _context(self) -> Generator[None, None, None]:
820826
active_message_pump.reset(message_pump_reset_token)
821827
active_app.reset(app_reset_token)
822828

829+
def get_pseudo_classes(self) -> Iterable[str]:
830+
"""Pseudo classes for a widget.
831+
832+
Returns:
833+
Names of the pseudo classes.
834+
"""
835+
yield "focus" if self.app_focus else "blur"
836+
yield "dark" if self.dark else "light"
837+
if self.is_inline:
838+
yield "inline"
839+
823840
def animate(
824841
self,
825842
attribute: str,
@@ -1084,17 +1101,17 @@ def watch_dark(self, dark: bool) -> None:
10841101
self.set_class(dark, "-dark-mode", update=False)
10851102
self.set_class(not dark, "-light-mode", update=False)
10861103
self._refresh_truecolor_filter(self.ansi_theme)
1087-
self.call_later(self.refresh_css)
1104+
self.call_next(self.refresh_css)
10881105

10891106
def watch_ansi_theme_dark(self, theme: TerminalTheme) -> None:
10901107
if self.dark:
10911108
self._refresh_truecolor_filter(theme)
1092-
self.call_later(self.refresh_css)
1109+
self.call_next(self.refresh_css)
10931110

10941111
def watch_ansi_theme_light(self, theme: TerminalTheme) -> None:
10951112
if not self.dark:
10961113
self._refresh_truecolor_filter(theme)
1097-
self.call_later(self.refresh_css)
1114+
self.call_next(self.refresh_css)
10981115

10991116
@property
11001117
def ansi_theme(self) -> TerminalTheme:
@@ -3130,7 +3147,10 @@ def refresh_css(self, animate: bool = True) -> None:
31303147
stylesheet.set_variables(self.get_css_variables())
31313148
stylesheet.reparse()
31323149
stylesheet.update(self.app, animate=animate)
3133-
self.screen._refresh_layout(self.size)
3150+
try:
3151+
self.screen._refresh_layout(self.size)
3152+
except ScreenError:
3153+
pass
31343154
# The other screens in the stack will need to know about some style
31353155
# changes, as a final pass let's check in on every screen that isn't
31363156
# the current one and update them too.
@@ -3614,6 +3634,7 @@ def post_mount() -> None:
36143634

36153635
def _watch_app_focus(self, focus: bool) -> None:
36163636
"""Respond to changes in app focus."""
3637+
self.screen._update_styles()
36173638
if focus:
36183639
# If we've got a last-focused widget, if it still has a screen,
36193640
# and if the screen is still the current screen and if nothing

src/textual/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,8 @@ def _get_textual_animations() -> AnimationLevel:
118118

119119
ESCAPE_DELAY: Final[float] = _get_environ_int("ESCDELAY", 100) / 1000.0
120120
"""The delay (in seconds) before reporting an escape key (not used if the extend key protocol is available)."""
121+
122+
SLOW_THRESHOLD: int = _get_environ_int("TEXTUAL_SLOW_THRESHOLD", 500)
123+
"""The time threshold (in milliseconds) after which a warning is logged
124+
if message processing exceeds this duration.
125+
"""

src/textual/css/_help_text.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -457,17 +457,19 @@ def dock_property_help_text(property_name: str, context: StylingContext) -> Help
457457
return HelpText(
458458
summary=f"Invalid value for [i]{property_name}[/] property",
459459
bullets=[
460-
Bullet("The value must be one of 'top', 'right', 'bottom' or 'left'"),
460+
Bullet(
461+
"The value must be one of 'top', 'right', 'bottom', 'left' or 'none'"
462+
),
461463
*ContextSpecificBullets(
462464
inline=[
463465
Bullet(
464-
"The 'dock' rule aligns a widget relative to the screen.",
466+
"The 'dock' rule attaches a widget to the edge of a container.",
465467
examples=[Example('header.styles.dock = "top"')],
466468
)
467469
],
468470
css=[
469471
Bullet(
470-
"The 'dock' rule aligns a widget relative to the screen.",
472+
"The 'dock' rule attaches a widget to the edge of a container.",
471473
examples=[Example("dock: top")],
472474
)
473475
],

0 commit comments

Comments
 (0)