mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'main' into package-docs
This commit is contained in:
@@ -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 `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 DOMNode.watch and DOMNode.is_attached methods https://github.com/Textualize/textual/pull/1750
|
||||
|
||||
### 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
|
||||
- `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
|
||||
|
||||
@@ -42,6 +44,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
### Removed
|
||||
|
||||
- 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
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
::: textual.widgets.Checkbox
|
||||
1
docs/api/switch.md
Normal file
1
docs/api/switch.md
Normal file
@@ -0,0 +1 @@
|
||||
::: textual.widgets.Switch
|
||||
@@ -288,7 +288,7 @@ So, thanks to this bit of code in my `Activity` widget...
|
||||
parent.move_child(
|
||||
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 )
|
||||
```
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Screen {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
Checkbox {
|
||||
Switch {
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ Checkbox {
|
||||
background: darkslategrey;
|
||||
}
|
||||
|
||||
#custom-design > .checkbox--switch {
|
||||
#custom-design > .switch--switch {
|
||||
color: dodgerblue;
|
||||
background: darkslateblue;
|
||||
}
|
||||
@@ -1,35 +1,35 @@
|
||||
from textual.app import App, ComposeResult
|
||||
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:
|
||||
yield Static("[b]Example checkboxes\n", classes="label")
|
||||
yield Static("[b]Example switches\n", classes="label")
|
||||
yield Horizontal(
|
||||
Static("off: ", classes="label"),
|
||||
Checkbox(animate=False),
|
||||
Switch(animate=False),
|
||||
classes="container",
|
||||
)
|
||||
yield Horizontal(
|
||||
Static("on: ", classes="label"),
|
||||
Checkbox(value=True),
|
||||
Switch(value=True),
|
||||
classes="container",
|
||||
)
|
||||
|
||||
focused_checkbox = Checkbox()
|
||||
focused_checkbox.focus()
|
||||
focused_switch = Switch()
|
||||
focused_switch.focus()
|
||||
yield Horizontal(
|
||||
Static("focused: ", classes="label"), focused_checkbox, classes="container"
|
||||
Static("focused: ", classes="label"), focused_switch, classes="container"
|
||||
)
|
||||
|
||||
yield Horizontal(
|
||||
Static("custom: ", classes="label"),
|
||||
Checkbox(id="custom-design"),
|
||||
Switch(id="custom-design"),
|
||||
classes="container",
|
||||
)
|
||||
|
||||
|
||||
app = CheckboxApp(css_path="checkbox.css")
|
||||
app = SwitchApp(css_path="switch.css")
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -40,7 +40,7 @@ Widgets are key to making user-friendly interfaces. The builtin widgets should c
|
||||
- [x] Buttons
|
||||
* [x] Error / warning variants
|
||||
- [ ] Color picker
|
||||
- [x] Checkbox
|
||||
- [ ] Checkbox
|
||||
- [ ] Content switcher
|
||||
- [x] DataTable
|
||||
* [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)
|
||||
- [ ] Radio boxes
|
||||
- [ ] Spark-lines
|
||||
- [X] Switch
|
||||
- [ ] Tabs
|
||||
- [ ] TextArea (multi-line input)
|
||||
* [ ] Basic controls
|
||||
|
||||
@@ -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
63
docs/widgets/switch.md
Normal 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
|
||||
@@ -121,7 +121,6 @@ nav:
|
||||
- "styles/width.md"
|
||||
- Widgets:
|
||||
- "widgets/button.md"
|
||||
- "widgets/checkbox.md"
|
||||
- "widgets/data_table.md"
|
||||
- "widgets/directory_tree.md"
|
||||
- "widgets/footer.md"
|
||||
@@ -133,6 +132,7 @@ nav:
|
||||
- "widgets/list_view.md"
|
||||
- "widgets/placeholder.md"
|
||||
- "widgets/static.md"
|
||||
- "widgets/switch.md"
|
||||
- "widgets/text_log.md"
|
||||
- "widgets/tree.md"
|
||||
- API:
|
||||
@@ -140,7 +140,6 @@ nav:
|
||||
- "api/app.md"
|
||||
- "api/binding.md"
|
||||
- "api/button.md"
|
||||
- "api/checkbox.md"
|
||||
- "api/color.md"
|
||||
- "api/containers.md"
|
||||
- "api/coordinate.md"
|
||||
@@ -165,6 +164,7 @@ nav:
|
||||
- "api/scroll_view.md"
|
||||
- "api/static.md"
|
||||
- "api/strip.md"
|
||||
- "api/switch.md"
|
||||
- "api/text_log.md"
|
||||
- "api/timer.md"
|
||||
- "api/tree.md"
|
||||
|
||||
@@ -5,7 +5,7 @@ from textual._easing import EASING
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.cli.previews.borders import TEXT
|
||||
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.widget import Widget
|
||||
from textual.widgets import Button, Footer, Label, Input
|
||||
@@ -23,8 +23,8 @@ class EasingButtons(Widget):
|
||||
|
||||
|
||||
class Bar(Widget):
|
||||
position = Reactive.init(START_POSITION)
|
||||
animation_running = Reactive(False)
|
||||
position = reactive(START_POSITION)
|
||||
animation_running = reactive(False)
|
||||
|
||||
DEFAULT_CSS = """
|
||||
|
||||
@@ -53,8 +53,8 @@ class Bar(Widget):
|
||||
|
||||
|
||||
class EasingApp(App):
|
||||
position = Reactive.init(START_POSITION)
|
||||
duration = Reactive.var(1.0)
|
||||
position = reactive(START_POSITION)
|
||||
duration = var(1.0)
|
||||
|
||||
def on_load(self):
|
||||
self.bind(
|
||||
|
||||
@@ -119,7 +119,7 @@ DarkSwitch .label {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
DarkSwitch Checkbox {
|
||||
DarkSwitch Switch {
|
||||
background: $boost;
|
||||
dock: left;
|
||||
}
|
||||
|
||||
@@ -15,15 +15,15 @@ from rich.text import Text
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Container, Horizontal
|
||||
from textual.reactive import reactive, watch
|
||||
from textual.reactive import reactive
|
||||
from textual.widgets import (
|
||||
Button,
|
||||
Checkbox,
|
||||
DataTable,
|
||||
Footer,
|
||||
Header,
|
||||
Input,
|
||||
Static,
|
||||
Switch,
|
||||
TextLog,
|
||||
)
|
||||
|
||||
@@ -138,7 +138,7 @@ Build your own or use the builtin widgets.
|
||||
|
||||
- **Input** Text / Password input.
|
||||
- **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.
|
||||
- **Tree** An generic tree with expandable nodes.
|
||||
- **DirectoryTree** A tree of file and folders.
|
||||
@@ -199,16 +199,16 @@ class Title(Static):
|
||||
|
||||
class DarkSwitch(Horizontal):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Checkbox(value=self.app.dark)
|
||||
yield Switch(value=self.app.dark)
|
||||
yield Static("Dark mode toggle", classes="label")
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from rich.tree import Tree
|
||||
|
||||
from ._context import NoActiveAppError
|
||||
from ._node_list import NodeList
|
||||
from ._types import CallbackType
|
||||
from .binding import Bindings, BindingType
|
||||
from .color import BLACK, WHITE, Color
|
||||
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.tokenize import IDENTIFIER
|
||||
from .message_pump import MessagePump
|
||||
from .reactive import Reactive
|
||||
from .reactive import Reactive, _watch
|
||||
from .timer import Timer
|
||||
from .walk import walk_breadth_first, walk_depth_first
|
||||
|
||||
@@ -210,6 +211,10 @@ class DOMNode(MessagePump):
|
||||
styles = self._component_styles[name]
|
||||
return styles
|
||||
|
||||
def _post_mount(self):
|
||||
"""Called after the object has been mounted."""
|
||||
Reactive._initialize_object(self)
|
||||
|
||||
@property
|
||||
def _node_bases(self) -> Iterator[Type[DOMNode]]:
|
||||
"""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]
|
||||
|
||||
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]:
|
||||
"""Get any pseudo classes applicable to this Node, e.g. hover, focus.
|
||||
|
||||
|
||||
@@ -123,6 +123,19 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
"""
|
||||
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:
|
||||
"""Set the parent, and therefore attach this node to the tree.
|
||||
|
||||
@@ -358,7 +371,10 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
finally:
|
||||
# This is critical, mount may be waiting
|
||||
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:
|
||||
"""Process messages until the queue is closed."""
|
||||
|
||||
@@ -53,6 +53,7 @@ class Pilot(Generic[ReturnType]):
|
||||
async def wait_for_scheduled_animations(self) -> None:
|
||||
"""Wait for any current and scheduled animations to complete."""
|
||||
await self._app.animator.wait_until_complete()
|
||||
await wait_for_idle(0)
|
||||
|
||||
async def exit(self, result: ReturnType) -> None:
|
||||
"""Exit the app with the given result.
|
||||
|
||||
@@ -7,36 +7,26 @@ from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Generic,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
import rich.repr
|
||||
|
||||
from . import events
|
||||
from ._callback import count_parameters, invoke
|
||||
from ._types import MessageTarget
|
||||
from ._callback import count_parameters
|
||||
from ._types import MessageTarget, CallbackType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .app import App
|
||||
from .widget import Widget
|
||||
from .dom import DOMNode
|
||||
|
||||
Reactable = Union[Widget, App]
|
||||
Reactable = DOMNode
|
||||
|
||||
ReactiveType = TypeVar("ReactiveType")
|
||||
|
||||
|
||||
class _NotSet:
|
||||
pass
|
||||
|
||||
|
||||
_NOT_SET = _NotSet()
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class Reactive(Generic[ReactiveType]):
|
||||
"""Reactive descriptor.
|
||||
@@ -50,7 +40,7 @@ class Reactive(Generic[ReactiveType]):
|
||||
compute: Run compute methods when attribute is changed. Defaults to True.
|
||||
"""
|
||||
|
||||
_reactives: TypeVar[dict[str, object]] = {}
|
||||
_reactives: ClassVar[dict[str, object]] = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -77,37 +67,6 @@ class Reactive(Generic[ReactiveType]):
|
||||
yield "always_update", self._always_update
|
||||
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
|
||||
def var(
|
||||
cls,
|
||||
@@ -254,7 +213,7 @@ class Reactive(Generic[ReactiveType]):
|
||||
|
||||
def invoke_watcher(
|
||||
watch_function: Callable, old_value: object, value: object
|
||||
) -> bool:
|
||||
) -> None:
|
||||
"""Invoke a watch function.
|
||||
|
||||
Args:
|
||||
@@ -262,8 +221,6 @@ class Reactive(Generic[ReactiveType]):
|
||||
old_value: The old 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
|
||||
param_count = count_parameters(watch_function)
|
||||
@@ -280,17 +237,23 @@ class Reactive(Generic[ReactiveType]):
|
||||
sender=obj, callback=partial(await_watcher, watch_result)
|
||||
)
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
watch_function = getattr(obj, f"watch_{name}", None)
|
||||
if callable(watch_function):
|
||||
invoke_watcher(watch_function, old_value, value)
|
||||
|
||||
watchers: list[Callable] = getattr(obj, "__watchers", {}).get(name, [])
|
||||
for watcher in watchers:
|
||||
invoke_watcher(watcher, old_value, value)
|
||||
# Process "global" watchers
|
||||
watchers: list[tuple[Reactable, Callable]]
|
||||
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
|
||||
def _compute(cls, obj: Reactable) -> None:
|
||||
@@ -362,10 +325,12 @@ class var(Reactive[ReactiveType]):
|
||||
)
|
||||
|
||||
|
||||
def watch(
|
||||
def _watch(
|
||||
node: DOMNode,
|
||||
obj: Reactable,
|
||||
attribute_name: str,
|
||||
callback: Callable[[Any], object],
|
||||
callback: CallbackType,
|
||||
*,
|
||||
init: bool = True,
|
||||
) -> None:
|
||||
"""Watch a reactive variable on an object.
|
||||
@@ -379,11 +344,11 @@ def watch(
|
||||
|
||||
if not hasattr(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, [])
|
||||
if callback in watcher_list:
|
||||
return
|
||||
watcher_list.append(callback)
|
||||
watcher_list.append((node, callback))
|
||||
if init:
|
||||
current_value = getattr(obj, attribute_name, None)
|
||||
Reactive._check_watchers(obj, attribute_name, current_value)
|
||||
|
||||
@@ -9,7 +9,6 @@ from ..case import camel_to_snake
|
||||
# be able to "see" them.
|
||||
if typing.TYPE_CHECKING:
|
||||
from ._button import Button
|
||||
from ._checkbox import Checkbox
|
||||
from ._data_table import DataTable
|
||||
from ._directory_tree import DirectoryTree
|
||||
from ._footer import Footer
|
||||
@@ -21,6 +20,7 @@ if typing.TYPE_CHECKING:
|
||||
from ._placeholder import Placeholder
|
||||
from ._pretty import Pretty
|
||||
from ._static import Static
|
||||
from ._switch import Switch
|
||||
from ._text_log import TextLog
|
||||
from ._tree import Tree
|
||||
from ._welcome import Welcome
|
||||
@@ -29,7 +29,7 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
__all__ = [
|
||||
"Button",
|
||||
"Checkbox",
|
||||
"Switch",
|
||||
"DataTable",
|
||||
"DirectoryTree",
|
||||
"Footer",
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
# This stub file must re-export every classes exposed in the __init__.py's `__all__` list:
|
||||
from ._button import Button as Button
|
||||
from ._data_table import DataTable as DataTable
|
||||
from ._checkbox import Checkbox as Checkbox
|
||||
from ._directory_tree import DirectoryTree as DirectoryTree
|
||||
from ._footer import Footer as Footer
|
||||
from ._header import Header as Header
|
||||
from ._input import Input as Input
|
||||
from ._label import Label as Label
|
||||
from ._list_view import ListView as ListView
|
||||
from ._list_item import ListItem as ListItem
|
||||
from ._placeholder import Placeholder as Placeholder
|
||||
from ._pretty import Pretty as Pretty
|
||||
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 ._tree import Tree as Tree
|
||||
from ._tree_node import TreeNode as TreeNode
|
||||
from ._welcome import Welcome as Welcome
|
||||
|
||||
@@ -11,7 +11,7 @@ from rich.text import Text, TextType
|
||||
from .. import events
|
||||
from ..css._error_tools import friendly_list
|
||||
from ..message import Message
|
||||
from ..reactive import Reactive
|
||||
from ..reactive import reactive
|
||||
from ..widgets import Static
|
||||
|
||||
|
||||
@@ -151,13 +151,13 @@ class Button(Static, can_focus=True):
|
||||
ACTIVE_EFFECT_DURATION = 0.3
|
||||
"""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."""
|
||||
|
||||
variant = Reactive.init("default")
|
||||
variant = reactive("default")
|
||||
"""The variant name for the button."""
|
||||
|
||||
disabled = Reactive(False)
|
||||
disabled = reactive(False)
|
||||
"""The disabled state of the button; `True` if disabled, `False` if not."""
|
||||
|
||||
class Pressed(Message, bubble=True):
|
||||
|
||||
@@ -1042,7 +1042,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
self._set_hover_cursor(True)
|
||||
if self.show_cursor and self.cursor_type != "none":
|
||||
# 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
|
||||
if meta:
|
||||
self.cursor_cell = Coordinate(meta["row"], meta["column"])
|
||||
|
||||
@@ -8,7 +8,7 @@ from rich.console import RenderableType
|
||||
from rich.text import Text
|
||||
|
||||
from .. import events
|
||||
from ..reactive import Reactive, watch
|
||||
from ..reactive import Reactive
|
||||
from ..widget import Widget
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class Footer(Widget):
|
||||
self.refresh()
|
||||
|
||||
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:
|
||||
self._key_text = None
|
||||
|
||||
@@ -5,7 +5,7 @@ from datetime import datetime
|
||||
from rich.text import Text
|
||||
|
||||
from ..widget import Widget
|
||||
from ..reactive import Reactive, watch
|
||||
from ..reactive import Reactive
|
||||
|
||||
|
||||
class HeaderIcon(Widget):
|
||||
@@ -133,5 +133,5 @@ class Header(Widget):
|
||||
def set_sub_title(sub_title: str) -> None:
|
||||
self.query_one(HeaderTitle).sub_text = sub_title
|
||||
|
||||
watch(self.app, "title", set_title)
|
||||
watch(self.app, "sub_title", set_sub_title)
|
||||
self.watch(self.app, "title", set_title)
|
||||
self.watch(self.app, "sub_title", set_sub_title)
|
||||
|
||||
@@ -12,12 +12,12 @@ from ..widget import Widget
|
||||
from ..scrollbar import ScrollBarRender
|
||||
|
||||
|
||||
class Checkbox(Widget, can_focus=True):
|
||||
"""A checkbox widget that represents a boolean value.
|
||||
class Switch(Widget, can_focus=True):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
@@ -27,20 +27,20 @@ class Checkbox(Widget, can_focus=True):
|
||||
"""
|
||||
| Key(s) | Description |
|
||||
| :- | :- |
|
||||
| enter,space | Toggle the checkbox status. |
|
||||
| enter,space | Toggle the switch state. |
|
||||
"""
|
||||
|
||||
COMPONENT_CLASSES: ClassVar[set[str]] = {
|
||||
"checkbox--switch",
|
||||
"switch--switch",
|
||||
}
|
||||
"""
|
||||
| Class | Description |
|
||||
| :- | :- |
|
||||
| `checkbox--switch` | Targets the switch of the checkbox. |
|
||||
| `switch--switch` | Targets the switch of the switch. |
|
||||
"""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
Checkbox {
|
||||
Switch {
|
||||
border: tall transparent;
|
||||
background: $panel;
|
||||
height: auto;
|
||||
@@ -48,49 +48,49 @@ class Checkbox(Widget, can_focus=True):
|
||||
padding: 0 2;
|
||||
}
|
||||
|
||||
Checkbox > .checkbox--switch {
|
||||
Switch > .switch--switch {
|
||||
background: $panel-darken-2;
|
||||
color: $panel-lighten-2;
|
||||
}
|
||||
|
||||
Checkbox:hover {
|
||||
Switch:hover {
|
||||
border: tall $background;
|
||||
}
|
||||
|
||||
Checkbox:focus {
|
||||
Switch:focus {
|
||||
border: tall $accent;
|
||||
}
|
||||
|
||||
Checkbox.-on {
|
||||
Switch.-on {
|
||||
|
||||
}
|
||||
|
||||
Checkbox.-on > .checkbox--switch {
|
||||
Switch.-on > .switch--switch {
|
||||
color: $success;
|
||||
}
|
||||
"""
|
||||
|
||||
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)
|
||||
"""The position of the slider."""
|
||||
|
||||
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.
|
||||
|
||||
Attributes:
|
||||
value: The value that the checkbox was changed to.
|
||||
input: The `Checkbox` widget that was changed.
|
||||
value: The value that the switch was changed to.
|
||||
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)
|
||||
self.value: bool = value
|
||||
self.input: Checkbox = sender
|
||||
self.input: Switch = sender
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -101,14 +101,14 @@ class Checkbox(Widget, can_focus=True):
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
):
|
||||
"""Initialise the checkbox.
|
||||
"""Initialise the switch.
|
||||
|
||||
Args:
|
||||
value: The initial value of the checkbox. Defaults to False.
|
||||
animate: True if the checkbox should animate when toggled. Defaults to True.
|
||||
name: The name of the checkbox.
|
||||
id: The ID of the checkbox in the DOM.
|
||||
classes: The CSS classes of the checkbox.
|
||||
value: The initial value of the switch. Defaults to False.
|
||||
animate: True if the switch should animate when toggled. Defaults to True.
|
||||
name: The name of the switch.
|
||||
id: The ID of the switch in the DOM.
|
||||
classes: The CSS classes of the switch.
|
||||
"""
|
||||
super().__init__(name=name, id=id, classes=classes)
|
||||
if value:
|
||||
@@ -128,7 +128,7 @@ class Checkbox(Widget, can_focus=True):
|
||||
self.set_class(slider_pos == 1, "-on")
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
style = self.get_component_rich_style("checkbox--switch")
|
||||
style = self.get_component_rich_style("switch--switch")
|
||||
return ScrollBarRender(
|
||||
virtual_size=100,
|
||||
window_size=50,
|
||||
@@ -150,6 +150,6 @@ class Checkbox(Widget, can_focus=True):
|
||||
self.toggle()
|
||||
|
||||
def toggle(self) -> None:
|
||||
"""Toggle the checkbox value. As a result of the value changing,
|
||||
a Checkbox.Changed message will be posted."""
|
||||
"""Toggle the switch value. As a result of the value changing,
|
||||
a Switch.Changed message will be posted."""
|
||||
self.value = not self.value
|
||||
File diff suppressed because one or more lines are too long
38
tests/snapshot_tests/snapshot_apps/screen_switch.py
Normal file
38
tests/snapshot_tests/snapshot_apps/screen_switch.py
Normal 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()
|
||||
@@ -52,8 +52,8 @@ def test_dock_layout_sidebar(snap_compare):
|
||||
# from these examples which test rendering and simple interactions with it.
|
||||
|
||||
|
||||
def test_checkboxes(snap_compare):
|
||||
"""Tests checkboxes but also acts a regression test for using
|
||||
def test_switches(snap_compare):
|
||||
"""Tests switches but also acts a regression test for using
|
||||
width: auto in a Horizontal layout context."""
|
||||
press = [
|
||||
"shift+tab",
|
||||
@@ -63,7 +63,7 @@ def test_checkboxes(snap_compare):
|
||||
"enter", # toggle on
|
||||
"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):
|
||||
@@ -214,3 +214,7 @@ def test_auto_width_input(snap_compare):
|
||||
assert snap_compare(
|
||||
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"])
|
||||
|
||||
@@ -152,21 +152,21 @@ def test_focus_next_and_previous_with_type_selector_without_self():
|
||||
screen = app.screen
|
||||
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.widgets import Button, Checkbox, Input
|
||||
from textual.widgets import Button, Input, Switch
|
||||
|
||||
screen._add_children(
|
||||
Vertical(
|
||||
Horizontal(
|
||||
Input(id="w3"),
|
||||
Checkbox(id="w4"),
|
||||
Switch(id="w4"),
|
||||
Input(id="w5"),
|
||||
Button(id="w6"),
|
||||
Checkbox(id="w7"),
|
||||
Switch(id="w7"),
|
||||
id="w2",
|
||||
),
|
||||
Horizontal(
|
||||
Button(id="w9"),
|
||||
Checkbox(id="w10"),
|
||||
Switch(id="w10"),
|
||||
Button(id="w11"),
|
||||
Input(id="w12"),
|
||||
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.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_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(Input).id == "w5"
|
||||
|
||||
|
||||
@@ -8,9 +8,13 @@ from textual.containers import Container
|
||||
async def test_remove_single_widget():
|
||||
"""It should be possible to the only widget on a screen."""
|
||||
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
|
||||
await pilot.app.query_one(Static).remove()
|
||||
assert not widget.is_attached
|
||||
assert len(pilot.app.screen.children) == 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user