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 `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

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(
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 )
```

View File

@@ -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;
}

View File

@@ -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()

View File

@@ -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

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"
- 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"

View File

@@ -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(

View File

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

View File

@@ -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

View File

@@ -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.

View File

@@ -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."""

View File

@@ -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.

View File

@@ -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)

View File

@@ -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",

View File

@@ -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

View File

@@ -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):

View File

@@ -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"])

View File

@@ -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

View File

@@ -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)

View File

@@ -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

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.
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"])

View File

@@ -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"

View File

@@ -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