Rename Checkbox to Switch

A new form of Checkbox will be arriving in Textual soon, working in
conjunction with a RadioButton. What was called Checkbox is perhaps a wee
bit heavyweight in terms of visual design, but is a style of widget that
should remain.

With this in mind we're renaming the current Checkbox to Switch. In all
other respects its workings remains the same, only the name has changed.

Things for people to watch out for:

- Imports will need to be updated.
- Queries will need to be updated; special attention will need to be paid to
  any queries that are string-based.
- CSS will need to be changed if any Checkbox styling is happening, or if
  any Checkbox component styles are being used.

See #1725 as the initial motivation and #1746 as the issue for this
particular change.
This commit is contained in:
Dave Pearson
2023-02-09 11:10:30 +00:00
parent b86882ed0c
commit decc1e2f3c
17 changed files with 288 additions and 286 deletions

View File

@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- 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

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

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

@@ -126,7 +126,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"
@@ -138,6 +137,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:
@@ -145,7 +145,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"
@@ -170,6 +169,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

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

@@ -18,7 +18,7 @@ from textual.containers import Container, Horizontal
from textual.reactive import reactive, watch from textual.reactive import reactive, watch
from textual.widgets import ( from textual.widgets import (
Button, Button,
Checkbox, Switch,
DataTable, DataTable,
Footer, Footer,
Header, Header,
@@ -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) 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

@@ -9,7 +9,7 @@ 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 ._switch import Switch
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
@@ -29,7 +29,7 @@ if typing.TYPE_CHECKING:
__all__ = [ __all__ = [
"Button", "Button",
"Checkbox", "Switch",
"DataTable", "DataTable",
"DirectoryTree", "DirectoryTree",
"Footer", "Footer",

View File

@@ -1,7 +1,7 @@
# 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 ._switch import Switch as Switch
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

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

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

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, Switch, Input
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"