Merge branch 'main' into package-docs

This commit is contained in:
Dave Pearson
2023-02-09 15:49:56 +00:00
29 changed files with 580 additions and 369 deletions

View File

@@ -19,11 +19,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally - Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally
- Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to <kbd>Space</kbd> https://github.com/Textualize/textual/issues/1433 - Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to <kbd>Space</kbd> https://github.com/Textualize/textual/issues/1433
- Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437 - Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437
- Added DOMNode.watch and DOMNode.is_attached methods https://github.com/Textualize/textual/pull/1750
### Changed ### Changed
- Breaking change: `TreeNode` can no longer be imported from `textual.widgets`; it is now available via `from textual.widgets.tree import TreeNode`. https://github.com/Textualize/textual/pull/1637 - Breaking change: `TreeNode` can no longer be imported from `textual.widgets`; it is now available via `from textual.widgets.tree import TreeNode`. https://github.com/Textualize/textual/pull/1637
- `Tree` now shows a (subdued) cursor for a highlighted node when focus has moved elsewhere https://github.com/Textualize/textual/issues/1471 - `Tree` now shows a (subdued) cursor for a highlighted node when focus has moved elsewhere https://github.com/Textualize/textual/issues/1471
- Breaking change: renamed `Checkbox` to `Switch`.
### Fixed ### Fixed
@@ -42,6 +44,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Removed ### Removed
- Methods `MessagePump.emit` and `MessagePump.emit_no_wait` https://github.com/Textualize/textual/pull/1738 - Methods `MessagePump.emit` and `MessagePump.emit_no_wait` https://github.com/Textualize/textual/pull/1738
- Removed `reactive.watch` in favor of DOMNode.watch.
## [0.10.1] - 2023-01-20 ## [0.10.1] - 2023-01-20

View File

@@ -1 +0,0 @@
::: textual.widgets.Checkbox

1
docs/api/switch.md Normal file
View File

@@ -0,0 +1 @@
::: textual.widgets.Switch

View File

@@ -288,7 +288,7 @@ So, thanks to this bit of code in my `Activity` widget...
parent.move_child( parent.move_child(
self, before=parent.children.index( self ) - 1 self, before=parent.children.index( self ) - 1
) )
self.post_message_no_wait( self.Moved( self ) ) self.emit_no_wait( self.Moved( self ) )
self.scroll_visible( top=True ) self.scroll_visible( top=True )
``` ```

View File

@@ -7,7 +7,7 @@ Screen {
width: auto; width: auto;
} }
Checkbox { Switch {
height: auto; height: auto;
width: auto; width: auto;
} }
@@ -22,7 +22,7 @@ Checkbox {
background: darkslategrey; background: darkslategrey;
} }
#custom-design > .checkbox--switch { #custom-design > .switch--switch {
color: dodgerblue; color: dodgerblue;
background: darkslateblue; background: darkslateblue;
} }

View File

@@ -1,35 +1,35 @@
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.containers import Horizontal from textual.containers import Horizontal
from textual.widgets import Checkbox, Static from textual.widgets import Switch, Static
class CheckboxApp(App): class SwitchApp(App):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Static("[b]Example checkboxes\n", classes="label") yield Static("[b]Example switches\n", classes="label")
yield Horizontal( yield Horizontal(
Static("off: ", classes="label"), Static("off: ", classes="label"),
Checkbox(animate=False), Switch(animate=False),
classes="container", classes="container",
) )
yield Horizontal( yield Horizontal(
Static("on: ", classes="label"), Static("on: ", classes="label"),
Checkbox(value=True), Switch(value=True),
classes="container", classes="container",
) )
focused_checkbox = Checkbox() focused_switch = Switch()
focused_checkbox.focus() focused_switch.focus()
yield Horizontal( yield Horizontal(
Static("focused: ", classes="label"), focused_checkbox, classes="container" Static("focused: ", classes="label"), focused_switch, classes="container"
) )
yield Horizontal( yield Horizontal(
Static("custom: ", classes="label"), Static("custom: ", classes="label"),
Checkbox(id="custom-design"), Switch(id="custom-design"),
classes="container", classes="container",
) )
app = CheckboxApp(css_path="checkbox.css") app = SwitchApp(css_path="switch.css")
if __name__ == "__main__": if __name__ == "__main__":
app.run() app.run()

View File

@@ -40,7 +40,7 @@ Widgets are key to making user-friendly interfaces. The builtin widgets should c
- [x] Buttons - [x] Buttons
* [x] Error / warning variants * [x] Error / warning variants
- [ ] Color picker - [ ] Color picker
- [x] Checkbox - [ ] Checkbox
- [ ] Content switcher - [ ] Content switcher
- [x] DataTable - [x] DataTable
* [x] Cell select * [x] Cell select
@@ -70,6 +70,7 @@ Widgets are key to making user-friendly interfaces. The builtin widgets should c
* [ ] Style variants (solid, thin etc) * [ ] Style variants (solid, thin etc)
- [ ] Radio boxes - [ ] Radio boxes
- [ ] Spark-lines - [ ] Spark-lines
- [X] Switch
- [ ] Tabs - [ ] Tabs
- [ ] TextArea (multi-line input) - [ ] TextArea (multi-line input)
* [ ] Basic controls * [ ] Basic controls

View File

@@ -1,63 +0,0 @@
# Checkbox
A simple checkbox widget which stores a boolean value.
- [x] Focusable
- [ ] Container
## Example
The example below shows checkboxes in various states.
=== "Output"
```{.textual path="docs/examples/widgets/checkbox.py"}
```
=== "checkbox.py"
```python
--8<-- "docs/examples/widgets/checkbox.py"
```
=== "checkbox.css"
```sass
--8<-- "docs/examples/widgets/checkbox.css"
```
## Reactive Attributes
| Name | Type | Default | Description |
| ------- | ------ | ------- | ---------------------------------- |
| `value` | `bool` | `False` | The default value of the checkbox. |
## Bindings
The checkbox widget defines directly the following bindings:
::: textual.widgets.Checkbox.BINDINGS
options:
show_root_heading: false
show_root_toc_entry: false
## Component Classes
The checkbox widget provides the following component classes:
::: textual.widgets.Checkbox.COMPONENT_CLASSES
options:
show_root_heading: false
show_root_toc_entry: false
## Messages
### ::: textual.widgets.Checkbox.Changed
## Additional Notes
- To remove the spacing around a checkbox, set `border: none;` and `padding: 0;`.
## See Also
- [Checkbox](../api/checkbox.md) code reference

63
docs/widgets/switch.md Normal file
View File

@@ -0,0 +1,63 @@
# Switch
A simple switch widget which stores a boolean value.
- [x] Focusable
- [ ] Container
## Example
The example below shows switches in various states.
=== "Output"
```{.textual path="docs/examples/widgets/switch.py"}
```
=== "switch.py"
```python
--8<-- "docs/examples/widgets/switch.py"
```
=== "switch.css"
```sass
--8<-- "docs/examples/widgets/switch.css"
```
## Reactive Attributes
| Name | Type | Default | Description |
|---------|--------|---------|----------------------------------|
| `value` | `bool` | `False` | The default value of the switch. |
## Bindings
The switch widget defines directly the following bindings:
::: textual.widgets.Switch.BINDINGS
options:
show_root_heading: false
show_root_toc_entry: false
## Component Classes
The switch widget provides the following component classes:
::: textual.widgets.Switch.COMPONENT_CLASSES
options:
show_root_heading: false
show_root_toc_entry: false
## Messages
### ::: textual.widgets.Switch.Changed
## Additional Notes
- To remove the spacing around a `Switch`, set `border: none;` and `padding: 0;`.
## See Also
- [Switch](../api/switch.md) code reference

View File

@@ -121,7 +121,6 @@ nav:
- "styles/width.md" - "styles/width.md"
- Widgets: - Widgets:
- "widgets/button.md" - "widgets/button.md"
- "widgets/checkbox.md"
- "widgets/data_table.md" - "widgets/data_table.md"
- "widgets/directory_tree.md" - "widgets/directory_tree.md"
- "widgets/footer.md" - "widgets/footer.md"
@@ -133,6 +132,7 @@ nav:
- "widgets/list_view.md" - "widgets/list_view.md"
- "widgets/placeholder.md" - "widgets/placeholder.md"
- "widgets/static.md" - "widgets/static.md"
- "widgets/switch.md"
- "widgets/text_log.md" - "widgets/text_log.md"
- "widgets/tree.md" - "widgets/tree.md"
- API: - API:
@@ -140,7 +140,6 @@ nav:
- "api/app.md" - "api/app.md"
- "api/binding.md" - "api/binding.md"
- "api/button.md" - "api/button.md"
- "api/checkbox.md"
- "api/color.md" - "api/color.md"
- "api/containers.md" - "api/containers.md"
- "api/coordinate.md" - "api/coordinate.md"
@@ -165,6 +164,7 @@ nav:
- "api/scroll_view.md" - "api/scroll_view.md"
- "api/static.md" - "api/static.md"
- "api/strip.md" - "api/strip.md"
- "api/switch.md"
- "api/text_log.md" - "api/text_log.md"
- "api/timer.md" - "api/timer.md"
- "api/tree.md" - "api/tree.md"

View File

@@ -5,7 +5,7 @@ from textual._easing import EASING
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.cli.previews.borders import TEXT from textual.cli.previews.borders import TEXT
from textual.containers import Container, Horizontal, Vertical from textual.containers import Container, Horizontal, Vertical
from textual.reactive import Reactive from textual.reactive import reactive, var
from textual.scrollbar import ScrollBarRender from textual.scrollbar import ScrollBarRender
from textual.widget import Widget from textual.widget import Widget
from textual.widgets import Button, Footer, Label, Input from textual.widgets import Button, Footer, Label, Input
@@ -23,8 +23,8 @@ class EasingButtons(Widget):
class Bar(Widget): class Bar(Widget):
position = Reactive.init(START_POSITION) position = reactive(START_POSITION)
animation_running = Reactive(False) animation_running = reactive(False)
DEFAULT_CSS = """ DEFAULT_CSS = """
@@ -53,8 +53,8 @@ class Bar(Widget):
class EasingApp(App): class EasingApp(App):
position = Reactive.init(START_POSITION) position = reactive(START_POSITION)
duration = Reactive.var(1.0) duration = var(1.0)
def on_load(self): def on_load(self):
self.bind( self.bind(

View File

@@ -119,7 +119,7 @@ DarkSwitch .label {
color: $text-muted; color: $text-muted;
} }
DarkSwitch Checkbox { DarkSwitch Switch {
background: $boost; background: $boost;
dock: left; dock: left;
} }

View File

@@ -15,15 +15,15 @@ from rich.text import Text
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.binding import Binding from textual.binding import Binding
from textual.containers import Container, Horizontal from textual.containers import Container, Horizontal
from textual.reactive import reactive, watch from textual.reactive import reactive
from textual.widgets import ( from textual.widgets import (
Button, Button,
Checkbox,
DataTable, DataTable,
Footer, Footer,
Header, Header,
Input, Input,
Static, Static,
Switch,
TextLog, TextLog,
) )
@@ -138,7 +138,7 @@ Build your own or use the builtin widgets.
- **Input** Text / Password input. - **Input** Text / Password input.
- **Button** Clickable button with a number of styles. - **Button** Clickable button with a number of styles.
- **Checkbox** A checkbox to toggle between states. - **Switch** A switch to toggle between states.
- **DataTable** A spreadsheet-like widget for navigating data. Cells may contain text or Rich renderables. - **DataTable** A spreadsheet-like widget for navigating data. Cells may contain text or Rich renderables.
- **Tree** An generic tree with expandable nodes. - **Tree** An generic tree with expandable nodes.
- **DirectoryTree** A tree of file and folders. - **DirectoryTree** A tree of file and folders.
@@ -199,16 +199,16 @@ class Title(Static):
class DarkSwitch(Horizontal): class DarkSwitch(Horizontal):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Checkbox(value=self.app.dark) yield Switch(value=self.app.dark)
yield Static("Dark mode toggle", classes="label") yield Static("Dark mode toggle", classes="label")
def on_mount(self) -> None: def on_mount(self) -> None:
watch(self.app, "dark", self.on_dark_change, init=False) self.watch(self.app, "dark", self.on_dark_change, init=False)
def on_dark_change(self, dark: bool) -> None: def on_dark_change(self, dark: bool) -> None:
self.query_one(Checkbox).value = self.app.dark self.query_one(Switch).value = self.app.dark
def on_checkbox_changed(self, event: Checkbox.Changed) -> None: def on_switch_changed(self, event: Switch.Changed) -> None:
self.app.dark = event.value self.app.dark = event.value

View File

@@ -22,6 +22,7 @@ from rich.tree import Tree
from ._context import NoActiveAppError from ._context import NoActiveAppError
from ._node_list import NodeList from ._node_list import NodeList
from ._types import CallbackType
from .binding import Bindings, BindingType from .binding import Bindings, BindingType
from .color import BLACK, WHITE, Color from .color import BLACK, WHITE, Color
from .css._error_tools import friendly_list from .css._error_tools import friendly_list
@@ -31,7 +32,7 @@ from .css.parse import parse_declarations
from .css.styles import RenderStyles, Styles from .css.styles import RenderStyles, Styles
from .css.tokenize import IDENTIFIER from .css.tokenize import IDENTIFIER
from .message_pump import MessagePump from .message_pump import MessagePump
from .reactive import Reactive from .reactive import Reactive, _watch
from .timer import Timer from .timer import Timer
from .walk import walk_breadth_first, walk_depth_first from .walk import walk_breadth_first, walk_depth_first
@@ -210,6 +211,10 @@ class DOMNode(MessagePump):
styles = self._component_styles[name] styles = self._component_styles[name]
return styles return styles
def _post_mount(self):
"""Called after the object has been mounted."""
Reactive._initialize_object(self)
@property @property
def _node_bases(self) -> Iterator[Type[DOMNode]]: def _node_bases(self) -> Iterator[Type[DOMNode]]:
"""Iterator[Type[DOMNode]]: The DOMNode bases classes (including self.__class__)""" """Iterator[Type[DOMNode]]: The DOMNode bases classes (including self.__class__)"""
@@ -643,6 +648,23 @@ class DOMNode(MessagePump):
""" """
return [child for child in self.children if child.display] return [child for child in self.children if child.display]
def watch(
self,
obj: DOMNode,
attribute_name: str,
callback: CallbackType,
init: bool = True,
) -> None:
"""Watches for modifications to reactive attributes on another object.
Args:
obj: Object containing attribute to watch.
attribute_name: Attribute to watch.
callback: A callback to run when attribute changes.
init: Check watchers on first call.
"""
_watch(self, obj, attribute_name, callback, init=init)
def get_pseudo_classes(self) -> Iterable[str]: def get_pseudo_classes(self) -> Iterable[str]:
"""Get any pseudo classes applicable to this Node, e.g. hover, focus. """Get any pseudo classes applicable to this Node, e.g. hover, focus.

View File

@@ -123,6 +123,19 @@ class MessagePump(metaclass=MessagePumpMeta):
""" """
return self.app._logger return self.app._logger
@property
def is_attached(self) -> bool:
"""Is the node is attached to the app via the DOM."""
from .app import App
node = self
while not isinstance(node, App):
if node._parent is None:
return False
node = node._parent
return True
def _attach(self, parent: MessagePump) -> None: def _attach(self, parent: MessagePump) -> None:
"""Set the parent, and therefore attach this node to the tree. """Set the parent, and therefore attach this node to the tree.
@@ -358,7 +371,10 @@ class MessagePump(metaclass=MessagePumpMeta):
finally: finally:
# This is critical, mount may be waiting # This is critical, mount may be waiting
self._mounted_event.set() self._mounted_event.set()
Reactive._initialize_object(self) self._post_mount()
def _post_mount(self):
"""Called after the object has been mounted."""
async def _process_messages_loop(self) -> None: async def _process_messages_loop(self) -> None:
"""Process messages until the queue is closed.""" """Process messages until the queue is closed."""

View File

@@ -53,6 +53,7 @@ class Pilot(Generic[ReturnType]):
async def wait_for_scheduled_animations(self) -> None: async def wait_for_scheduled_animations(self) -> None:
"""Wait for any current and scheduled animations to complete.""" """Wait for any current and scheduled animations to complete."""
await self._app.animator.wait_until_complete() await self._app.animator.wait_until_complete()
await wait_for_idle(0)
async def exit(self, result: ReturnType) -> None: async def exit(self, result: ReturnType) -> None:
"""Exit the app with the given result. """Exit the app with the given result.

View File

@@ -7,36 +7,26 @@ from typing import (
Any, Any,
Awaitable, Awaitable,
Callable, Callable,
ClassVar,
Generic, Generic,
Type, Type,
TypeVar, TypeVar,
Union,
) )
import rich.repr import rich.repr
from . import events from . import events
from ._callback import count_parameters, invoke from ._callback import count_parameters
from ._types import MessageTarget from ._types import MessageTarget, CallbackType
if TYPE_CHECKING: if TYPE_CHECKING:
from .app import App from .dom import DOMNode
from .widget import Widget
Reactable = Union[Widget, App] Reactable = DOMNode
ReactiveType = TypeVar("ReactiveType") ReactiveType = TypeVar("ReactiveType")
class _NotSet:
pass
_NOT_SET = _NotSet()
T = TypeVar("T")
@rich.repr.auto @rich.repr.auto
class Reactive(Generic[ReactiveType]): class Reactive(Generic[ReactiveType]):
"""Reactive descriptor. """Reactive descriptor.
@@ -50,7 +40,7 @@ class Reactive(Generic[ReactiveType]):
compute: Run compute methods when attribute is changed. Defaults to True. compute: Run compute methods when attribute is changed. Defaults to True.
""" """
_reactives: TypeVar[dict[str, object]] = {} _reactives: ClassVar[dict[str, object]] = {}
def __init__( def __init__(
self, self,
@@ -77,37 +67,6 @@ class Reactive(Generic[ReactiveType]):
yield "always_update", self._always_update yield "always_update", self._always_update
yield "compute", self._run_compute yield "compute", self._run_compute
@classmethod
def init(
cls,
default: ReactiveType | Callable[[], ReactiveType],
*,
layout: bool = False,
repaint: bool = True,
always_update: bool = False,
compute: bool = True,
) -> Reactive:
"""A reactive variable that calls watchers and compute on initialize (post mount).
Args:
default: A default value or callable that returns a default.
layout: Perform a layout on change. Defaults to False.
repaint: Perform a repaint on change. Defaults to True.
always_update: Call watchers even when the new value equals the old value. Defaults to False.
compute: Run compute methods when attribute is changed. Defaults to True.
Returns:
A Reactive instance which calls watchers or initialize.
"""
return cls(
default,
layout=layout,
repaint=repaint,
init=True,
always_update=always_update,
compute=compute,
)
@classmethod @classmethod
def var( def var(
cls, cls,
@@ -254,7 +213,7 @@ class Reactive(Generic[ReactiveType]):
def invoke_watcher( def invoke_watcher(
watch_function: Callable, old_value: object, value: object watch_function: Callable, old_value: object, value: object
) -> bool: ) -> None:
"""Invoke a watch function. """Invoke a watch function.
Args: Args:
@@ -262,8 +221,6 @@ class Reactive(Generic[ReactiveType]):
old_value: The old value of the attribute. old_value: The old value of the attribute.
value: The new value of the attribute. value: The new value of the attribute.
Returns:
True if the watcher was run, or False if it was posted.
""" """
_rich_traceback_omit = True _rich_traceback_omit = True
param_count = count_parameters(watch_function) param_count = count_parameters(watch_function)
@@ -280,17 +237,23 @@ class Reactive(Generic[ReactiveType]):
sender=obj, callback=partial(await_watcher, watch_result) sender=obj, callback=partial(await_watcher, watch_result)
) )
) )
return False
else:
return True
watch_function = getattr(obj, f"watch_{name}", None) watch_function = getattr(obj, f"watch_{name}", None)
if callable(watch_function): if callable(watch_function):
invoke_watcher(watch_function, old_value, value) invoke_watcher(watch_function, old_value, value)
watchers: list[Callable] = getattr(obj, "__watchers", {}).get(name, []) # Process "global" watchers
for watcher in watchers: watchers: list[tuple[Reactable, Callable]]
invoke_watcher(watcher, old_value, value) watchers = getattr(obj, "__watchers", {}).get(name, [])
# Remove any watchers for reactables that have since closed
if watchers:
watchers[:] = [
(reactable, callback)
for reactable, callback in watchers
if reactable.is_attached and not reactable._closing
]
for _, callback in watchers:
invoke_watcher(callback, old_value, value)
@classmethod @classmethod
def _compute(cls, obj: Reactable) -> None: def _compute(cls, obj: Reactable) -> None:
@@ -362,10 +325,12 @@ class var(Reactive[ReactiveType]):
) )
def watch( def _watch(
node: DOMNode,
obj: Reactable, obj: Reactable,
attribute_name: str, attribute_name: str,
callback: Callable[[Any], object], callback: CallbackType,
*,
init: bool = True, init: bool = True,
) -> None: ) -> None:
"""Watch a reactive variable on an object. """Watch a reactive variable on an object.
@@ -379,11 +344,11 @@ def watch(
if not hasattr(obj, "__watchers"): if not hasattr(obj, "__watchers"):
setattr(obj, "__watchers", {}) setattr(obj, "__watchers", {})
watchers: dict[str, list[Callable]] = getattr(obj, "__watchers") watchers: dict[str, list[tuple[Reactable, Callable]]] = getattr(obj, "__watchers")
watcher_list = watchers.setdefault(attribute_name, []) watcher_list = watchers.setdefault(attribute_name, [])
if callback in watcher_list: if callback in watcher_list:
return return
watcher_list.append(callback) watcher_list.append((node, callback))
if init: if init:
current_value = getattr(obj, attribute_name, None) current_value = getattr(obj, attribute_name, None)
Reactive._check_watchers(obj, attribute_name, current_value) Reactive._check_watchers(obj, attribute_name, current_value)

View File

@@ -9,7 +9,6 @@ from ..case import camel_to_snake
# be able to "see" them. # be able to "see" them.
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from ._button import Button from ._button import Button
from ._checkbox import Checkbox
from ._data_table import DataTable from ._data_table import DataTable
from ._directory_tree import DirectoryTree from ._directory_tree import DirectoryTree
from ._footer import Footer from ._footer import Footer
@@ -21,6 +20,7 @@ if typing.TYPE_CHECKING:
from ._placeholder import Placeholder from ._placeholder import Placeholder
from ._pretty import Pretty from ._pretty import Pretty
from ._static import Static from ._static import Static
from ._switch import Switch
from ._text_log import TextLog from ._text_log import TextLog
from ._tree import Tree from ._tree import Tree
from ._welcome import Welcome from ._welcome import Welcome
@@ -29,7 +29,7 @@ if typing.TYPE_CHECKING:
__all__ = [ __all__ = [
"Button", "Button",
"Checkbox", "Switch",
"DataTable", "DataTable",
"DirectoryTree", "DirectoryTree",
"Footer", "Footer",

View File

@@ -1,18 +1,17 @@
# This stub file must re-export every classes exposed in the __init__.py's `__all__` list: # This stub file must re-export every classes exposed in the __init__.py's `__all__` list:
from ._button import Button as Button from ._button import Button as Button
from ._data_table import DataTable as DataTable from ._data_table import DataTable as DataTable
from ._checkbox import Checkbox as Checkbox
from ._directory_tree import DirectoryTree as DirectoryTree from ._directory_tree import DirectoryTree as DirectoryTree
from ._footer import Footer as Footer from ._footer import Footer as Footer
from ._header import Header as Header from ._header import Header as Header
from ._input import Input as Input
from ._label import Label as Label from ._label import Label as Label
from ._list_view import ListView as ListView from ._list_view import ListView as ListView
from ._list_item import ListItem as ListItem from ._list_item import ListItem as ListItem
from ._placeholder import Placeholder as Placeholder from ._placeholder import Placeholder as Placeholder
from ._pretty import Pretty as Pretty from ._pretty import Pretty as Pretty
from ._static import Static as Static from ._static import Static as Static
from ._input import Input as Input from ._switch import Switch as Switch
from ._text_log import TextLog as TextLog from ._text_log import TextLog as TextLog
from ._tree import Tree as Tree from ._tree import Tree as Tree
from ._tree_node import TreeNode as TreeNode
from ._welcome import Welcome as Welcome from ._welcome import Welcome as Welcome

View File

@@ -11,7 +11,7 @@ from rich.text import Text, TextType
from .. import events from .. import events
from ..css._error_tools import friendly_list from ..css._error_tools import friendly_list
from ..message import Message from ..message import Message
from ..reactive import Reactive from ..reactive import reactive
from ..widgets import Static from ..widgets import Static
@@ -151,13 +151,13 @@ class Button(Static, can_focus=True):
ACTIVE_EFFECT_DURATION = 0.3 ACTIVE_EFFECT_DURATION = 0.3
"""When buttons are clicked they get the `-active` class for this duration (in seconds)""" """When buttons are clicked they get the `-active` class for this duration (in seconds)"""
label: Reactive[RenderableType] = Reactive("") label: reactive[RenderableType] = reactive("")
"""The text label that appears within the button.""" """The text label that appears within the button."""
variant = Reactive.init("default") variant = reactive("default")
"""The variant name for the button.""" """The variant name for the button."""
disabled = Reactive(False) disabled = reactive(False)
"""The disabled state of the button; `True` if disabled, `False` if not.""" """The disabled state of the button; `True` if disabled, `False` if not."""
class Pressed(Message, bubble=True): class Pressed(Message, bubble=True):

View File

@@ -1042,7 +1042,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
self._set_hover_cursor(True) self._set_hover_cursor(True)
if self.show_cursor and self.cursor_type != "none": if self.show_cursor and self.cursor_type != "none":
# Only post selection events if there is a visible row/col/cell cursor. # Only post selection events if there is a visible row/col/cell cursor.
self._post_message_selected_message() self._post_selected_message()
meta = self.get_style_at(event.x, event.y).meta meta = self.get_style_at(event.x, event.y).meta
if meta: if meta:
self.cursor_cell = Coordinate(meta["row"], meta["column"]) self.cursor_cell = Coordinate(meta["row"], meta["column"])

View File

@@ -8,7 +8,7 @@ from rich.console import RenderableType
from rich.text import Text from rich.text import Text
from .. import events from .. import events
from ..reactive import Reactive, watch from ..reactive import Reactive
from ..widget import Widget from ..widget import Widget
@@ -66,7 +66,7 @@ class Footer(Widget):
self.refresh() self.refresh()
def on_mount(self) -> None: def on_mount(self) -> None:
watch(self.screen, "focused", self._focus_changed) self.watch(self.screen, "focused", self._focus_changed)
def _focus_changed(self, focused: Widget | None) -> None: def _focus_changed(self, focused: Widget | None) -> None:
self._key_text = None self._key_text = None

View File

@@ -5,7 +5,7 @@ from datetime import datetime
from rich.text import Text from rich.text import Text
from ..widget import Widget from ..widget import Widget
from ..reactive import Reactive, watch from ..reactive import Reactive
class HeaderIcon(Widget): class HeaderIcon(Widget):
@@ -133,5 +133,5 @@ class Header(Widget):
def set_sub_title(sub_title: str) -> None: def set_sub_title(sub_title: str) -> None:
self.query_one(HeaderTitle).sub_text = sub_title self.query_one(HeaderTitle).sub_text = sub_title
watch(self.app, "title", set_title) self.watch(self.app, "title", set_title)
watch(self.app, "sub_title", set_sub_title) self.watch(self.app, "sub_title", set_sub_title)

View File

@@ -12,12 +12,12 @@ from ..widget import Widget
from ..scrollbar import ScrollBarRender from ..scrollbar import ScrollBarRender
class Checkbox(Widget, can_focus=True): class Switch(Widget, can_focus=True):
"""A checkbox widget that represents a boolean value. """A switch widget that represents a boolean value.
Can be toggled by clicking on it or through its [bindings][textual.widgets.Checkbox.BINDINGS]. Can be toggled by clicking on it or through its [bindings][textual.widgets.Switch.BINDINGS].
The checkbox widget also contains [component classes][textual.widgets.Checkbox.COMPONENT_CLASSES] The switch widget also contains [component classes][textual.widgets.Switch.COMPONENT_CLASSES]
that enable more customization. that enable more customization.
""" """
@@ -27,20 +27,20 @@ class Checkbox(Widget, can_focus=True):
""" """
| Key(s) | Description | | Key(s) | Description |
| :- | :- | | :- | :- |
| enter,space | Toggle the checkbox status. | | enter,space | Toggle the switch state. |
""" """
COMPONENT_CLASSES: ClassVar[set[str]] = { COMPONENT_CLASSES: ClassVar[set[str]] = {
"checkbox--switch", "switch--switch",
} }
""" """
| Class | Description | | Class | Description |
| :- | :- | | :- | :- |
| `checkbox--switch` | Targets the switch of the checkbox. | | `switch--switch` | Targets the switch of the switch. |
""" """
DEFAULT_CSS = """ DEFAULT_CSS = """
Checkbox { Switch {
border: tall transparent; border: tall transparent;
background: $panel; background: $panel;
height: auto; height: auto;
@@ -48,49 +48,49 @@ class Checkbox(Widget, can_focus=True):
padding: 0 2; padding: 0 2;
} }
Checkbox > .checkbox--switch { Switch > .switch--switch {
background: $panel-darken-2; background: $panel-darken-2;
color: $panel-lighten-2; color: $panel-lighten-2;
} }
Checkbox:hover { Switch:hover {
border: tall $background; border: tall $background;
} }
Checkbox:focus { Switch:focus {
border: tall $accent; border: tall $accent;
} }
Checkbox.-on { Switch.-on {
} }
Checkbox.-on > .checkbox--switch { Switch.-on > .switch--switch {
color: $success; color: $success;
} }
""" """
value = reactive(False, init=False) value = reactive(False, init=False)
"""The value of the checkbox; `True` for on and `False` for off.""" """The value of the switch; `True` for on and `False` for off."""
slider_pos = reactive(0.0) slider_pos = reactive(0.0)
"""The position of the slider.""" """The position of the slider."""
class Changed(Message, bubble=True): class Changed(Message, bubble=True):
"""Posted when the status of the checkbox changes. """Posted when the status of the switch changes.
Can be handled using `on_checkbox_changed` in a subclass of `Checkbox` Can be handled using `on_switch_changed` in a subclass of `Switch`
or in a parent widget in the DOM. or in a parent widget in the DOM.
Attributes: Attributes:
value: The value that the checkbox was changed to. value: The value that the switch was changed to.
input: The `Checkbox` widget that was changed. input: The `Switch` widget that was changed.
""" """
def __init__(self, sender: Checkbox, value: bool) -> None: def __init__(self, sender: Switch, value: bool) -> None:
super().__init__(sender) super().__init__(sender)
self.value: bool = value self.value: bool = value
self.input: Checkbox = sender self.input: Switch = sender
def __init__( def __init__(
self, self,
@@ -101,14 +101,14 @@ class Checkbox(Widget, can_focus=True):
id: str | None = None, id: str | None = None,
classes: str | None = None, classes: str | None = None,
): ):
"""Initialise the checkbox. """Initialise the switch.
Args: Args:
value: The initial value of the checkbox. Defaults to False. value: The initial value of the switch. Defaults to False.
animate: True if the checkbox should animate when toggled. Defaults to True. animate: True if the switch should animate when toggled. Defaults to True.
name: The name of the checkbox. name: The name of the switch.
id: The ID of the checkbox in the DOM. id: The ID of the switch in the DOM.
classes: The CSS classes of the checkbox. classes: The CSS classes of the switch.
""" """
super().__init__(name=name, id=id, classes=classes) super().__init__(name=name, id=id, classes=classes)
if value: if value:
@@ -128,7 +128,7 @@ class Checkbox(Widget, can_focus=True):
self.set_class(slider_pos == 1, "-on") self.set_class(slider_pos == 1, "-on")
def render(self) -> RenderableType: def render(self) -> RenderableType:
style = self.get_component_rich_style("checkbox--switch") style = self.get_component_rich_style("switch--switch")
return ScrollBarRender( return ScrollBarRender(
virtual_size=100, virtual_size=100,
window_size=50, window_size=50,
@@ -150,6 +150,6 @@ class Checkbox(Widget, can_focus=True):
self.toggle() self.toggle()
def toggle(self) -> None: def toggle(self) -> None:
"""Toggle the checkbox value. As a result of the value changing, """Toggle the switch value. As a result of the value changing,
a Checkbox.Changed message will be posted.""" a Switch.Changed message will be posted."""
self.value = not self.value self.value = not self.value

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,38 @@
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Static, Header, Footer
class ScreenA(Screen):
BINDINGS = [("b", "switch_to_b", "Switch to screen B")]
def compose(self) -> ComposeResult:
yield Header()
yield Static("A")
yield Footer()
def action_switch_to_b(self):
self.app.switch_screen(ScreenB())
class ScreenB(Screen):
def compose(self) -> ComposeResult:
yield Header()
yield Static("B")
yield Footer()
class ModalApp(App):
BINDINGS = [("a", "push_a", "Push screen A")]
def compose(self) -> ComposeResult:
yield Header()
yield Footer()
def action_push_a(self) -> None:
self.push_screen(ScreenA())
if __name__ == "__main__":
app = ModalApp()
app.run()

View File

@@ -52,8 +52,8 @@ def test_dock_layout_sidebar(snap_compare):
# from these examples which test rendering and simple interactions with it. # from these examples which test rendering and simple interactions with it.
def test_checkboxes(snap_compare): def test_switches(snap_compare):
"""Tests checkboxes but also acts a regression test for using """Tests switches but also acts a regression test for using
width: auto in a Horizontal layout context.""" width: auto in a Horizontal layout context."""
press = [ press = [
"shift+tab", "shift+tab",
@@ -63,7 +63,7 @@ def test_checkboxes(snap_compare):
"enter", # toggle on "enter", # toggle on
"wait:20", "wait:20",
] ]
assert snap_compare(WIDGET_EXAMPLES_DIR / "checkbox.py", press=press) assert snap_compare(WIDGET_EXAMPLES_DIR / "switch.py", press=press)
def test_input_and_focus(snap_compare): def test_input_and_focus(snap_compare):
@@ -214,3 +214,7 @@ def test_auto_width_input(snap_compare):
assert snap_compare( assert snap_compare(
SNAPSHOT_APPS_DIR / "auto_width_input.py", press=["tab", *"Hello"] SNAPSHOT_APPS_DIR / "auto_width_input.py", press=["tab", *"Hello"]
) )
def test_screen_switch(snap_compare):
assert snap_compare(SNAPSHOT_APPS_DIR / "screen_switch.py", press=["a", "b"])

View File

@@ -152,21 +152,21 @@ def test_focus_next_and_previous_with_type_selector_without_self():
screen = app.screen screen = app.screen
from textual.containers import Horizontal, Vertical from textual.containers import Horizontal, Vertical
from textual.widgets import Button, Checkbox, Input from textual.widgets import Button, Input, Switch
screen._add_children( screen._add_children(
Vertical( Vertical(
Horizontal( Horizontal(
Input(id="w3"), Input(id="w3"),
Checkbox(id="w4"), Switch(id="w4"),
Input(id="w5"), Input(id="w5"),
Button(id="w6"), Button(id="w6"),
Checkbox(id="w7"), Switch(id="w7"),
id="w2", id="w2",
), ),
Horizontal( Horizontal(
Button(id="w9"), Button(id="w9"),
Checkbox(id="w10"), Switch(id="w10"),
Button(id="w11"), Button(id="w11"),
Input(id="w12"), Input(id="w12"),
Input(id="w13"), Input(id="w13"),
@@ -180,11 +180,11 @@ def test_focus_next_and_previous_with_type_selector_without_self():
assert screen.focused.id == "w3" assert screen.focused.id == "w3"
assert screen.focus_next(Button).id == "w6" assert screen.focus_next(Button).id == "w6"
assert screen.focus_next(Checkbox).id == "w7" assert screen.focus_next(Switch).id == "w7"
assert screen.focus_next(Input).id == "w12" assert screen.focus_next(Input).id == "w12"
assert screen.focus_previous(Button).id == "w11" assert screen.focus_previous(Button).id == "w11"
assert screen.focus_previous(Checkbox).id == "w10" assert screen.focus_previous(Switch).id == "w10"
assert screen.focus_previous(Button).id == "w9" assert screen.focus_previous(Button).id == "w9"
assert screen.focus_previous(Input).id == "w5" assert screen.focus_previous(Input).id == "w5"

View File

@@ -8,9 +8,13 @@ from textual.containers import Container
async def test_remove_single_widget(): async def test_remove_single_widget():
"""It should be possible to the only widget on a screen.""" """It should be possible to the only widget on a screen."""
async with App().run_test() as pilot: async with App().run_test() as pilot:
await pilot.app.mount(Static()) widget = Static()
assert not widget.is_attached
await pilot.app.mount(widget)
assert widget.is_attached
assert len(pilot.app.screen.children) == 1 assert len(pilot.app.screen.children) == 1
await pilot.app.query_one(Static).remove() await pilot.app.query_one(Static).remove()
assert not widget.is_attached
assert len(pilot.app.screen.children) == 0 assert len(pilot.app.screen.children) == 0