Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to TextUI are documented here.

### Fixed

- Focus cues now blend over native and author backgrounds instead of replacing them, so variant buttons retain their colors. Compact inputs, selects, and text areas also preserve their backgrounds; tint strengths remain 15% by default and 25% for compact editable controls.
- Clicking a data-table column heading now sorts once per click. The table's click and mouse-move handlers also called `super()`, which Textual had already run, so every heading click was delivered twice and the ascending/descending toggle always ended on descending. A column-resize press no longer sorts either, and no longer swallows the next heading click when the drag is released.
- Controller resize hooks now receive terminal dimensions even when the active screen has padding or a border.
- Declared autofocus now waits for hidden ancestors to be displayed and runs again when controls are revealed after startup. Disabled controls and background screens do not take focus; anonymous modal autofocus registrations are released on dismissal.
Expand Down
2 changes: 1 addition & 1 deletion docs/controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Static tab views use native `TabbedContent`:
</tabbed-content>
```

Each pane needs an ID and title. `initial` must name one of those panes; without it, Textual selects the first. The `tab-activated` event provides `context.event.pane`. Tabs respond to native mouse and keyboard input. Add `autofocus="true"` to a focusable control when it should receive keyboard input after the document or a modal opens, or after a hidden control or its parent is shown with `display=True`. Focus waits for layout, skips disabled and hidden controls, and applies only to the active screen. In an inactive tab, autofocus waits for that tab to activate and does not override `initial`. TextUI gives focused native controls an accent background tint by default; application TCSS may override it. The [controls example](../examples/controls/app.ui) shows all four controls in a runnable project.
Each pane needs an ID and title. `initial` must name one of those panes; without it, Textual selects the first. The `tab-activated` event provides `context.event.pane`. Tabs respond to native mouse and keyboard input. Add `autofocus="true"` to a focusable control when it should receive keyboard input after the document or a modal opens, or after a hidden control or its parent is shown with `display=True`. Focus waits for layout, skips disabled and hidden controls, and applies only to the active screen. In an inactive tab, autofocus waits for that tab to activate and does not override `initial`. TextUI gives focused native controls an accent `background-tint` that preserves their own backgrounds and button variant colors; application TCSS may [adjust or disable the tint](project-runtime.md). The [controls example](../examples/controls/app.ui) shows all four controls in a runnable project.

## Choices, disclosure, and indicators

Expand Down
15 changes: 14 additions & 1 deletion docs/project-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,20 @@ Use `<style preset="compact"/>` when an application needs denser native controls
</ui>
```

`compact` is opt-in; documents without it retain Textual's normal control density. Presets have no body and only accept the `preset` attribute. A linked controller can switch any declared preset while the application runs. The method returns its new enabled state and reapplies the host stylesheet plus all active document blocks in their original order:
`compact` is opt-in; documents without it retain Textual's normal control density. Presets have no body and only accept the `preset` attribute.

Focus uses native `background-tint`, preserving button variants and author backgrounds. The default accent tint is 15%; compact inputs, selects, and text areas use 25%. Adjust it in application TCSS, after presets, with a focused selector:

```css
Button:focus, Input:focus, Select:focus, TextArea:focus,
DataTable:focus, Tree:focus, RuntimeList:focus {
background-tint: $accent 6%;
}
```

Use `background-tint: transparent` to disable TextUI's tint for selected controls while retaining their other native focus styling. Changing only `background` does not disable the tint.

A linked controller can switch any declared preset while the application runs. The method returns its new enabled state and reapplies the host stylesheet plus all active document blocks in their original order:

```python
@command(label="Compact", shortcut="ctrl+d")
Expand Down
52 changes: 52 additions & 0 deletions tests/test_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,55 @@ async def test_focused_compact_controls_keep_content_across_preset_switches():
await pilot.pause()
rendered = "\n".join(_screen_rows(app)[choices.region.y:choices.region.bottom])
assert "First" in rendered and "Last" in rendered


@pytest.mark.asyncio
async def test_focus_preserves_button_variant_colors_when_compact_is_toggled():
app = textui.TextUI(textui.DocumentLoader().from_string('''<ui>
<style preset="compact"/>
<button id="go" variant="primary">Go</button>
<button id="stop" variant="error">Stop</button>
</ui>'''))
app.theme = "textual-dark"
async with app.run_test(size=(40, 10)) as pilot:
enabled = True
for compact in (True, False, True):
if compact != enabled:
enabled = app.document.toggle_style_preset("compact")
for name, dominant in (("go", "b"), ("stop", "r")):
button = app.document.get_by_id(name)
button.focus()
await pilot.pause()
focused = button.background_colors[1]
others = [getattr(focused, channel) for channel in "rgb" if channel != dominant]
assert getattr(focused, dominant) > max(others), (name, compact, focused)


@pytest.mark.asyncio
@pytest.mark.parametrize("markup", [
'<input id="control" value="Ready"/>',
'<select id="control" value="ready"><option value="ready">Ready</option></select>',
'<text-area id="control" style="height: 4;">Ready</text-area>',
])
async def test_focus_tints_author_backgrounds_without_replacing_them(markup):
app = textui.TextUI(textui.DocumentLoader().from_string(f'''<ui>
<style preset="compact"/>
<style>Input, Select, TextArea {{ background: #0178d4; }}</style>
{markup}
<button id="other">Other</button>
</ui>'''))
app.theme = "textual-dark"
async with app.run_test(size=(40, 12)) as pilot:
control = app.document.get_by_id("control")
enabled = True
for compact in (True, False, True):
if compact != enabled:
enabled = app.document.toggle_style_preset("compact")
app.document.get_by_id("other").focus()
await pilot.pause()
resting = control.background_colors[1]
control.focus()
await pilot.pause()
focused = control.background_colors[1]
assert focused.b > max(focused.r, focused.g), (markup, compact, focused)
assert focused != resting
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions tests/visual/__snapshots__/darwin/showcase-shell-120x50.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions tests/visual/__snapshots__/darwin/showcase-shell-80x24.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions tests/visual/__snapshots__/linux/showcase-shell-120x50.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions tests/visual/__snapshots__/linux/showcase-shell-80x24.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions textui/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@
}
Input:focus, Select:focus, TextArea:focus {
outline: none;
background: $accent 25%;
background-tint: $accent 25%;
}
"""

FOCUS_TCSS = """
/* Blend over native variant and author backgrounds instead of replacing them. */
Button:focus, Input:focus, Select:focus, TextArea:focus, Checkbox:focus,
Switch:focus, RadioButton:focus, RadioSet:focus, DataTable:focus,
Tree:focus, RuntimeList:focus {
background: $accent 15%;
background-tint: $accent 15%;
}
"""

Expand Down
Loading