From c7b729ce2224011f2f79606c05a11010effcb9e6 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Sat, 22 Jul 2023 16:22:18 +0200 Subject: [PATCH 1/5] add DigitDisplay for digits and basic arithmetic operators --- Makefile | 2 +- docs/examples/widgets/digit_display.py | 24 ++ src/textual/widgets/__init__.py | 2 + src/textual/widgets/_digit_display.py | 269 ++++++++++++++++++ .../__snapshots__/test_snapshots.ambr | 155 ++++++++++ tests/snapshot_tests/test_snapshots.py | 5 +- tests/test_digit_display.py | 16 ++ 7 files changed, 470 insertions(+), 3 deletions(-) create mode 100644 docs/examples/widgets/digit_display.py create mode 100644 src/textual/widgets/_digit_display.py create mode 100644 tests/test_digit_display.py diff --git a/Makefile b/Makefile index a663100bbc..94cd6c9f43 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ unit-test: .PHONY: test-snapshot-update test-snapshot-update: - $(run) pytest --cov-report term-missing --cov=textual tests/ -vv --snapshot-update + $(run) pytest --cov-report term-missing --cov=textual tests/ -vv --snapshot-update $(PYTEST_ARGS) .PHONY: typecheck typecheck: diff --git a/docs/examples/widgets/digit_display.py b/docs/examples/widgets/digit_display.py new file mode 100644 index 0000000000..e135782bea --- /dev/null +++ b/docs/examples/widgets/digit_display.py @@ -0,0 +1,24 @@ +from textual.app import App, ComposeResult +from textual.widgets import Static +from textual.widgets import DigitDisplay + + +class MyApp(App): + BINDINGS = [] + + def compose(self) -> ComposeResult: + yield Static("Digits: 0123456789") + yield DigitDisplay("0123456789") + + punctuation=" .+,XYZ^*/-=" + yield Static("Punctuation: " + punctuation) + yield DigitDisplay(punctuation) + + equation = "x = y^2 + 3.14159*y + 10" + yield Static("Equation: " + equation) + yield DigitDisplay(equation) + + +if __name__ == "__main__": + app = MyApp() + app.run() diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index a66c80935c..12c7071957 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -40,6 +40,7 @@ from ._tooltip import Tooltip from ._tree import Tree from ._welcome import Welcome + from ._digit_display import DigitDisplay __all__ = [ @@ -76,6 +77,7 @@ "Tooltip", "Tree", "Welcome", + "DigitDisplay", ] _WIDGETS_LAZY_LOADING_CACHE: dict[str, type[Widget]] = {} diff --git a/src/textual/widgets/_digit_display.py b/src/textual/widgets/_digit_display.py new file mode 100644 index 0000000000..cb8aaa3747 --- /dev/null +++ b/src/textual/widgets/_digit_display.py @@ -0,0 +1,269 @@ +from textual.app import ComposeResult +from textual.reactive import reactive +from textual.widget import Widget +from textual.widgets import Static + + +_character_map: dict[str, str] = {} + +# in the mappings below, we use underscores to make spaces more visible, +# we will strip them out later +_character_map[ + "0" +] = """ +┏━┓ +┃╱┃ +┗━┛ +""" + +_character_map[ + "1" +] = """ +·┓· + ┃· +╺┻╸ +""" + +_character_map[ + "2" +] = """ +╺━┓ +┏━┛ +┗━╸ +""" + +_character_map[ + "3" +] = """ +╺━┓ + ━┫ +╺━┛ +""" + +_character_map[ + "4" +] = """ +╻ ╻ +┗━┫ + ╹ +""" + +_character_map[ + "5" +] = """ +┏━╸ +┗━┓ +╺━┛ +""" + +_character_map[ + "6" +] = """ +┏━╸ +┣━┓ +┗━┛ +""" + +_character_map[ + "7" +] = """ +╺━┓ + ╹ + ╹· +""" + +_character_map[ + "8" +] = """ +┏━┓ +┣━┫ +┗━┛ +""" + +_character_map[ + "9" +] = """ +┏━┓ +┗━┫ +╺━┛ +""" + +_character_map[ + " " +] = """ +··· +··· +··· +""" + + +_character_map[ + "X" +] = """ +╻ ╻ +·╋· +╹ ╹ +""" + +_character_map[ + "Y" +] = """ +╻ ╻ +·┳· +·╹· +""" + +_character_map[ + "Z" +] = """ +╺━┓ +·▞· +┗━╸ +""" + + +_character_map[ + "." +] = """ +·· +·· +·• +""" + + +_character_map[ + "," +] = """ +·· +·· +·▞ +""" + +_character_map[ + "+" +] = """ +··· +╺╋╸ +··· +""" + +_character_map[ + "-" +] = """ +··· +╺━╸ +··· +""" + +_character_map[ + "=" +] = """ +··· +╺━· +╺━· +""" + +_character_map[ + "*" +] = """ +··· +·✱· +··· +""" + + +_character_map[ + "/" +] = """ +··╻ +·▞· +╹·· +""" + +_character_map[ + "^" +] = """ +·╻· +▝·▘ +··· +""" + + +_VIRTUAL_SPACE = "·" + +# here we strip spaces and replace virtual spaces with spaces +_character_map = { + k: v.strip().replace(_VIRTUAL_SPACE, " ") for k, v in _character_map.items() +} + + +class SingleDigitDisplay(Static): + digit = reactive(" ", layout=True) + + DEFAULT_CSS = """ + SingleDigitDisplay { + height: 3; + min-width: 2; + max-width: 3; + } + """ + + def __init__(self, initial_value=" ", **kwargs): + super().__init__(**kwargs) + self.digit = initial_value + + def watch_digit(self, digit: str) -> None: + """Called when the digit attribute changes.""" + if len(digit) > 1: + raise ValueError(f"Expected a single character, got {len(digit)}") + self.update(_character_map[digit.upper()]) + + +class DigitDisplay(Widget): + """A widget to display digits and basic arithmetic operators using Unicode blocks.""" + + digits = reactive("", layout=True) + + DEFAULT_CSS = """ + DigitDisplay { + layout: horizontal; + height: 3; + } + """ + + def __init__(self, initial_value="", **kwargs): + super().__init__(**kwargs) + self._displays = [SingleDigitDisplay(d) for d in initial_value] + self.digits = initial_value + + def compose(self) -> ComposeResult: + for widget in self._displays: + yield widget + + def _add_digit_widget(self, digit: str) -> None: + new_widget = SingleDigitDisplay(digit) + self._displays.append(new_widget) + self.mount(new_widget) + + def watch_digits(self, digits: str) -> None: + """ + Called when the digits attribute changes. + Here we update the display widgets to match the input digits. + """ + diff_digits_len = len(digits) - len(self._displays) + + # Here we add or remove widgets to match the number of digits + if diff_digits_len > 0: + start = len(self._displays) + for i in range(diff_digits_len): + self._add_digit_widget(digits[start + i]) + elif diff_digits_len < 0: + for display in self._displays[diff_digits_len:]: + self._displays.remove(display) + display.remove() + + # At this point, the number of widgets matches the number of digits, and we can + # update the contents of the widgets that might need it + for i, d in enumerate(self.digits): + if self._displays[i].digit != d: + self._displays[i].digit = d diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index bc17cd2f98..0e4c3890f5 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -13719,6 +13719,161 @@ ''' # --- +# name: test_digit_display + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyApp + + + + + + + + + + Digits: 0123456789 + ┏━┓ ┓ ╺━┓╺━┓╻ ╻┏━╸┏━╸╺━┓┏━┓┏━┓ + ┃╱┃ ┃ ┏━┛ ━┫┗━┫┗━┓┣━┓  ╹┣━┫┗━┫ + ┗━┛╺┻╸┗━╸╺━┛  ╹╺━┛┗━┛ ╹ ┗━┛╺━┛ + Punctuation:  .+,XYZ^*/-= + ╻ ╻╻ ╻╺━┓ ╻   ╻ + ╺╋╸ ╋  ┳  ▞ ▝ ▘ ✱  ▞ ╺━╸╺━  +  • ▞╹ ╹ ╹ ┗━╸╹  ╺━  + Equation: x = y^2 + 3.14159*y + 10 + ╻ ╻╻ ╻ ╻ ╺━┓╺━┓ ┓ ╻ ╻ ┓ ┏━╸┏━┓╻ ╻ ┓ ┏━┓ +  ╋ ╺━  ┳ ▝ ▘┏━┛╺╋╸ ━┫ ┃ ┗━┫ ┃ ┗━┓┗━┫ ✱  ┳ ╺╋╸ ┃ ┃╱┃ + ╹ ╹╺━  ╹ ┗━╸╺━┛ •╺┻╸  ╹╺┻╸╺━┛╺━┛ ╹ ╺┻╸┗━┛ + + + + + + + + + + + + + + + + + ''' +# --- # name: test_disabled_widgets ''' diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index b798c0115b..e002b0f2e8 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -590,10 +590,11 @@ def test_notifications_through_screens(snap_compare) -> None: def test_notifications_through_modes(snap_compare) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "notification_through_modes.py") - def test_print_capture(snap_compare) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "capture_print.py") - def test_text_log_blank_write(snap_compare) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "text_log_blank_write.py") + +def test_digit_display(snap_compare) -> None: + assert snap_compare(WIDGET_EXAMPLES_DIR / "digit_display.py") diff --git a/tests/test_digit_display.py b/tests/test_digit_display.py new file mode 100644 index 0000000000..e544f5cb65 --- /dev/null +++ b/tests/test_digit_display.py @@ -0,0 +1,16 @@ +from textual.app import App, ComposeResult +from textual.widgets import DigitDisplay + + +async def test_digit_display_when_content_updated_then_display_widgets_update_accordingly(): + class DigitDisplayApp(App): + def compose(self) -> ComposeResult: + yield DigitDisplay("3.14159") + + app = DigitDisplayApp() + + async with app.run_test() as pilot: + w: DigitDisplay = app.query_one(DigitDisplay) + assert w.digits == "3.14159" + assert len(w._displays) == len(w.digits) + assert w.digits == "".join(w.digit for w in w._displays) From 8276758535ead11405ac92fed6ec4a923d511480 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Sun, 23 Jul 2023 16:17:58 +0200 Subject: [PATCH 2/5] add documentation and examples DigitDisplay widget --- CHANGELOG.md | 1 + .../widgets/digit_display_reacting.py | 20 ++++++ docs/widget_gallery.md | 9 +++ docs/widgets/digit_display.md | 65 +++++++++++++++++++ src/textual/widgets/_digit_display.py | 10 +-- 5 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 docs/examples/widgets/digit_display_reacting.py create mode 100644 docs/widgets/digit_display.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 24fe9573a1..1fe5e3b637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added the ability to run async methods as thread workers https://github.com/Textualize/textual/pull/2938 - Added `App.stop_animation` https://github.com/Textualize/textual/issues/2786 - Added `Widget.stop_animation` https://github.com/Textualize/textual/issues/2786 +- Added DigitDisplay widget https://github.com/Textualize/textual/pull/2995 ### Changed diff --git a/docs/examples/widgets/digit_display_reacting.py b/docs/examples/widgets/digit_display_reacting.py new file mode 100644 index 0000000000..46318a1e13 --- /dev/null +++ b/docs/examples/widgets/digit_display_reacting.py @@ -0,0 +1,20 @@ +from textual.app import App, ComposeResult +from textual.widgets import Input +from textual.widgets import DigitDisplay + + +class MyApp(App): + BINDINGS = [] + + def compose(self) -> ComposeResult: + yield Input(placeholder="Type something:") + yield DigitDisplay("") + + def on_input_changed(self, event: Input.Changed) -> None: + display: DigitDisplay = self.query_one(DigitDisplay) + display.digits = "".join(d for d in event.value if d in DigitDisplay.supported_digits) + + +if __name__ == "__main__": + app = MyApp() + app.run() diff --git a/docs/widget_gallery.md b/docs/widget_gallery.md index 18d2fd00d3..1451c5d3ca 100644 --- a/docs/widget_gallery.md +++ b/docs/widget_gallery.md @@ -52,6 +52,15 @@ A powerful data table, with configurable cursors. ```{.textual path="docs/examples/widgets/data_table.py"} ``` +## DigitDisplay + +A widget to display digits and basic arithmetic operators using Unicode blocks. + +[DigitDisplay reference](./widgets/digit_display.md){ .md-button .md-button--primary } + +```{.textual path="docs/examples/widgets/digit_display.py"} +``` + ## DirectoryTree A tree view of files and folders. diff --git a/docs/widgets/digit_display.md b/docs/widgets/digit_display.md new file mode 100644 index 0000000000..e451aee146 --- /dev/null +++ b/docs/widgets/digit_display.md @@ -0,0 +1,65 @@ +# DigitDisplay + +A widget to display digits and basic arithmetic operators using Unicode blocks. + +- [ ] Focusable +- [ ] Container + +## Examples + +=== "Static example" + + ```{.textual path="docs/examples/widgets/digit_display.py"} + ``` + +=== "digit_display.py" + + ```python + --8<-- "docs/examples/widgets/digit_display.py" + ``` + +=== "Reacting to an input" + + ```{.textual path="docs/examples/widgets/digit_display_reacting.py"} + ``` + +=== "digit_display_reacting.py" + + ```python + --8<-- "docs/examples/widgets/digit_display_reacting.py" + ``` + +## Reactive attributes + +| Name | Type | Default | Description | +| ------ | ------ | ------- | ---------------------------------------------- | +| `digits` | `str` | `""` | Use this to update the digits to be displayed. | + + +## Read-only attributes + +| Name | Type | Description | +| ------ | ------ | ----------------------------------------- | +| `supported_digits` | `frozenset[str]` | Contains the list of supported digits/characters. + +## Messages + +This widget sends no messages. + + +## Bindings + +This widget defines no bindings. + + +## Component classes + +This widget provides no component classes. + + +--- + + +::: textual.widgets.DigitDisplay + options: + heading_level: 2 diff --git a/src/textual/widgets/_digit_display.py b/src/textual/widgets/_digit_display.py index cb8aaa3747..fdbb8d9215 100644 --- a/src/textual/widgets/_digit_display.py +++ b/src/textual/widgets/_digit_display.py @@ -197,11 +197,11 @@ } -class SingleDigitDisplay(Static): +class _SingleDigitDisplay(Static): digit = reactive(" ", layout=True) DEFAULT_CSS = """ - SingleDigitDisplay { + _SingleDigitDisplay { height: 3; min-width: 2; max-width: 3; @@ -224,6 +224,8 @@ class DigitDisplay(Widget): digits = reactive("", layout=True) + supported_digits = frozenset(_character_map.keys()) + DEFAULT_CSS = """ DigitDisplay { layout: horizontal; @@ -233,7 +235,7 @@ class DigitDisplay(Widget): def __init__(self, initial_value="", **kwargs): super().__init__(**kwargs) - self._displays = [SingleDigitDisplay(d) for d in initial_value] + self._displays = [_SingleDigitDisplay(d) for d in initial_value] self.digits = initial_value def compose(self) -> ComposeResult: @@ -241,7 +243,7 @@ def compose(self) -> ComposeResult: yield widget def _add_digit_widget(self, digit: str) -> None: - new_widget = SingleDigitDisplay(digit) + new_widget = _SingleDigitDisplay(digit) self._displays.append(new_widget) self.mount(new_widget) From a5192253ba34703dddd5eb5877a68c2aacfb11ab Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Tue, 25 Jul 2023 20:52:28 +0200 Subject: [PATCH 3/5] DigitDisplay: followup on PR review --- docs/examples/widgets/digit_display.py | 10 +- docs/widgets/digit_display.md | 24 ++-- src/textual/widgets/_digit_display.py | 36 +++--- .../__snapshots__/test_snapshots.ambr | 108 +++++++++--------- 4 files changed, 92 insertions(+), 86 deletions(-) diff --git a/docs/examples/widgets/digit_display.py b/docs/examples/widgets/digit_display.py index e135782bea..c2d95e08e1 100644 --- a/docs/examples/widgets/digit_display.py +++ b/docs/examples/widgets/digit_display.py @@ -1,5 +1,5 @@ from textual.app import App, ComposeResult -from textual.widgets import Static +from textual.widgets import Label from textual.widgets import DigitDisplay @@ -7,15 +7,15 @@ class MyApp(App): BINDINGS = [] def compose(self) -> ComposeResult: - yield Static("Digits: 0123456789") + yield Label("Digits: 0123456789") yield DigitDisplay("0123456789") - punctuation=" .+,XYZ^*/-=" - yield Static("Punctuation: " + punctuation) + punctuation = " .+,XYZ^*/-=" + yield Label("Punctuation: " + punctuation) yield DigitDisplay(punctuation) equation = "x = y^2 + 3.14159*y + 10" - yield Static("Equation: " + equation) + yield Label("Equation: " + equation) yield DigitDisplay(equation) diff --git a/docs/widgets/digit_display.md b/docs/widgets/digit_display.md index e451aee146..594e6cee54 100644 --- a/docs/widgets/digit_display.md +++ b/docs/widgets/digit_display.md @@ -7,7 +7,10 @@ A widget to display digits and basic arithmetic operators using Unicode blocks. ## Examples -=== "Static example" + +### Static display example + +=== "Screenshot" ```{.textual path="docs/examples/widgets/digit_display.py"} ``` @@ -18,9 +21,12 @@ A widget to display digits and basic arithmetic operators using Unicode blocks. --8<-- "docs/examples/widgets/digit_display.py" ``` -=== "Reacting to an input" - ```{.textual path="docs/examples/widgets/digit_display_reacting.py"} +### Reacting to an input + +=== "Screenshot" + + ```{.textual path="docs/examples/widgets/digit_display_reacting.py" press="1,2,3"} ``` === "digit_display_reacting.py" @@ -31,16 +37,16 @@ A widget to display digits and basic arithmetic operators using Unicode blocks. ## Reactive attributes -| Name | Type | Default | Description | -| ------ | ------ | ------- | ---------------------------------------------- | -| `digits` | `str` | `""` | Use this to update the digits to be displayed. | +| Name | Type | Default | Description | +| ------ | ------ | ------- | ---------------------------------------------- | +| `digits` | `str` | `""` | Use this to update the digits to be displayed. | ## Read-only attributes -| Name | Type | Description | -| ------ | ------ | ----------------------------------------- | -| `supported_digits` | `frozenset[str]` | Contains the list of supported digits/characters. +| Name | Type | Description | +| ------ | ------ | ----------------------------------------- | +| `supported_digits` | `frozenset[str]` | Contains the list of supported digits/characters. | ## Messages diff --git a/src/textual/widgets/_digit_display.py b/src/textual/widgets/_digit_display.py index fdbb8d9215..c58769af29 100644 --- a/src/textual/widgets/_digit_display.py +++ b/src/textual/widgets/_digit_display.py @@ -199,6 +199,7 @@ class _SingleDigitDisplay(Static): digit = reactive(" ", layout=True) + """The digit to display.""" DEFAULT_CSS = """ _SingleDigitDisplay { @@ -212,10 +213,17 @@ def __init__(self, initial_value=" ", **kwargs): super().__init__(**kwargs) self.digit = initial_value - def watch_digit(self, digit: str) -> None: - """Called when the digit attribute changes.""" + def validate_digit(self, digit: str) -> str: + """Sanitize and validate the digit input.""" if len(digit) > 1: raise ValueError(f"Expected a single character, got {len(digit)}") + digit = digit.upper() + if digit not in _character_map: + raise ValueError(f"Unsupported character: {digit}") + return digit + + def _watch_digit(self, digit: str) -> None: + """Called when the digit attribute changes and passes validation.""" self.update(_character_map[digit.upper()]) @@ -223,8 +231,10 @@ class DigitDisplay(Widget): """A widget to display digits and basic arithmetic operators using Unicode blocks.""" digits = reactive("", layout=True) + """The digits to display.""" supported_digits = frozenset(_character_map.keys()) + """The digits and characters supported by this widget.""" DEFAULT_CSS = """ DigitDisplay { @@ -242,27 +252,17 @@ def compose(self) -> ComposeResult: for widget in self._displays: yield widget - def _add_digit_widget(self, digit: str) -> None: - new_widget = _SingleDigitDisplay(digit) - self._displays.append(new_widget) - self.mount(new_widget) - - def watch_digits(self, digits: str) -> None: + def _watch_digits(self, digits: str) -> None: """ Called when the digits attribute changes. Here we update the display widgets to match the input digits. """ - diff_digits_len = len(digits) - len(self._displays) - # Here we add or remove widgets to match the number of digits - if diff_digits_len > 0: - start = len(self._displays) - for i in range(diff_digits_len): - self._add_digit_widget(digits[start + i]) - elif diff_digits_len < 0: - for display in self._displays[diff_digits_len:]: - self._displays.remove(display) - display.remove() + while len(self._displays) < len(digits): + self._displays.append(_SingleDigitDisplay(digits[len(self._displays)])) + self.mount(self._displays[-1]) + while len(self._displays) > len(digits): + self._displays.pop().remove() # At this point, the number of widgets matches the number of digits, and we can # update the contents of the widgets that might need it diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 0e4c3890f5..7b86a6f4af 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -13742,131 +13742,131 @@ font-weight: 700; } - .terminal-1756784214-matrix { + .terminal-4012950226-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1756784214-title { + .terminal-4012950226-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1756784214-r1 { fill: #e1e1e1 } - .terminal-1756784214-r2 { fill: #c5c8c6 } + .terminal-4012950226-r1 { fill: #e1e1e1 } + .terminal-4012950226-r2 { fill: #c5c8c6 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + MyApp - + - - Digits: 0123456789 - ┏━┓ ┓ ╺━┓╺━┓╻ ╻┏━╸┏━╸╺━┓┏━┓┏━┓ - ┃╱┃ ┃ ┏━┛ ━┫┗━┫┗━┓┣━┓  ╹┣━┫┗━┫ - ┗━┛╺┻╸┗━╸╺━┛  ╹╺━┛┗━┛ ╹ ┗━┛╺━┛ - Punctuation:  .+,XYZ^*/-= - ╻ ╻╻ ╻╺━┓ ╻   ╻ - ╺╋╸ ╋  ┳  ▞ ▝ ▘ ✱  ▞ ╺━╸╺━  -  • ▞╹ ╹ ╹ ┗━╸╹  ╺━  - Equation: x = y^2 + 3.14159*y + 10 - ╻ ╻╻ ╻ ╻ ╺━┓╺━┓ ┓ ╻ ╻ ┓ ┏━╸┏━┓╻ ╻ ┓ ┏━┓ -  ╋ ╺━  ┳ ▝ ▘┏━┛╺╋╸ ━┫ ┃ ┗━┫ ┃ ┗━┓┗━┫ ✱  ┳ ╺╋╸ ┃ ┃╱┃ - ╹ ╹╺━  ╹ ┗━╸╺━┛ •╺┻╸  ╹╺┻╸╺━┛╺━┛ ╹ ╺┻╸┗━┛ - - - - - - - - - - - + + Digits: 0123456789 + ┏━┓ ┓ ╺━┓╺━┓╻ ╻┏━╸┏━╸╺━┓┏━┓┏━┓ + ┃╱┃ ┃ ┏━┛ ━┫┗━┫┗━┓┣━┓  ╹┣━┫┗━┫ + ┗━┛╺┻╸┗━╸╺━┛  ╹╺━┛┗━┛ ╹ ┗━┛╺━┛ + Punctuation:  .+,XYZ^*/-= + ╻ ╻╻ ╻╺━┓ ╻   ╻ + ╺╋╸ ╋  ┳  ▞ ▝ ▘ ✱  ▞ ╺━╸╺━  +  • ▞╹ ╹ ╹ ┗━╸╹  ╺━  + Equation: x = y^2 + 3.14159*y + 10 + ╻ ╻╻ ╻ ╻ ╺━┓╺━┓ ┓ ╻ ╻ ┓ ┏━╸┏━┓╻ ╻ ┓ ┏━┓ +  ╋ ╺━  ┳ ▝ ▘┏━┛╺╋╸ ━┫ ┃ ┗━┫ ┃ ┗━┓┗━┫ ✱  ┳ ╺╋╸ ┃ ┃╱┃ + ╹ ╹╺━  ╹ ┗━╸╺━┛ •╺┻╸  ╹╺┻╸╺━┛╺━┛ ╹ ╺┻╸┗━┛ + + + + + + + + + + + From 982d244abe6a1824b62ceb792b274c319b6bf1a5 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Tue, 25 Jul 2023 21:55:11 +0200 Subject: [PATCH 4/5] DigitDisplay: more followup on PR comments --- src/textual/widgets/__init__.py | 2 +- src/textual/widgets/_digit_display.py | 102 +++++++++++++--- .../__snapshots__/test_snapshots.ambr | 110 +++++++++--------- 3 files changed, 140 insertions(+), 74 deletions(-) diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index 12c7071957..50f04fea32 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -48,6 +48,7 @@ "Checkbox", "ContentSwitcher", "DataTable", + "DigitDisplay", "DirectoryTree", "Footer", "Header", @@ -77,7 +78,6 @@ "Tooltip", "Tree", "Welcome", - "DigitDisplay", ] _WIDGETS_LAZY_LOADING_CACHE: dict[str, type[Widget]] = {} diff --git a/src/textual/widgets/_digit_display.py b/src/textual/widgets/_digit_display.py index c58769af29..671c0efbb7 100644 --- a/src/textual/widgets/_digit_display.py +++ b/src/textual/widgets/_digit_display.py @@ -2,12 +2,15 @@ from textual.reactive import reactive from textual.widget import Widget from textual.widgets import Static +from ..geometry import Size _character_map: dict[str, str] = {} -# in the mappings below, we use underscores to make spaces more visible, -# we will strip them out later +_VIRTUAL_SPACE = "·" + +# in the mappings below, we use a dot instead of spaces to make them more +# visible, we will strip them out later _character_map[ "0" ] = """ @@ -158,9 +161,9 @@ _character_map[ "=" ] = """ -··· -╺━· -╺━· +·· +╺━ +╺━ """ _character_map[ @@ -189,8 +192,6 @@ """ -_VIRTUAL_SPACE = "·" - # here we strip spaces and replace virtual spaces with spaces _character_map = { k: v.strip().replace(_VIRTUAL_SPACE, " ") for k, v in _character_map.items() @@ -198,20 +199,52 @@ class _SingleDigitDisplay(Static): + """ + A widget to display a single digit or basic arithmetic symbol using Unicode blocks. + """ + digit = reactive(" ", layout=True) """The digit to display.""" DEFAULT_CSS = """ - _SingleDigitDisplay { - height: 3; - min-width: 2; - max-width: 3; - } + _SingleDigitDisplay { + height: 3; + min-width: 2; + max-width: 3; + } """ - def __init__(self, initial_value=" ", **kwargs): - super().__init__(**kwargs) + def __init__( + self, + initial_value: str = " ", + expand: bool = False, + shrink: bool = False, + markup: bool = True, + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + ): + """ + Create a single digit display widget. + + Example: + ```py + class Example(App): + def compose(self) -> ComposeResult: + return _SingleDigitDisplay("1") + """ + super().__init__( + expand=expand, + shrink=shrink, + markup=markup, + name=name, + id=id, + classes=classes, + disabled=disabled, + ) self.digit = initial_value + self._content_width = 3 def validate_digit(self, digit: str) -> str: """Sanitize and validate the digit input.""" @@ -224,11 +257,21 @@ def validate_digit(self, digit: str) -> str: def _watch_digit(self, digit: str) -> None: """Called when the digit attribute changes and passes validation.""" - self.update(_character_map[digit.upper()]) + content = _character_map[digit.upper()] + self._content_width = len(content.splitlines()[0]) + self.update(content) + + def get_content_width(self, container: Size, viewport: Size) -> int: + return self._content_width + + def get_content_height(self, container: Size, viewport: Size, width: int) -> int: + return 3 class DigitDisplay(Widget): - """A widget to display digits and basic arithmetic operators using Unicode blocks.""" + """ + A widget to display digits and basic arithmetic symbols using Unicode blocks. + """ digits = reactive("", layout=True) """The digits to display.""" @@ -243,8 +286,31 @@ class DigitDisplay(Widget): } """ - def __init__(self, initial_value="", **kwargs): - super().__init__(**kwargs) + def __init__( + self, + initial_value: str = "", + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + ): + """ + Create a Digit Display widget. + + Example: + ```py + class Example(App): + def compose(self) -> ComposeResult: + return DigitDisplay("123+456") + + Args: + initial_value (str, optional): The initial value to display. Defaults to "". + name: The name of the widget. + id: The ID of the widget in the DOM. + classes: The CSS classes for the widget. + disabled: Whether the widget is disabled or not. + """ + super().__init__(name=name, id=id, classes=classes, disabled=disabled) self._displays = [_SingleDigitDisplay(d) for d in initial_value] self.digits = initial_value diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 7b86a6f4af..857eb23b61 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -13742,131 +13742,131 @@ font-weight: 700; } - .terminal-4012950226-matrix { + .terminal-3468730299-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4012950226-title { + .terminal-3468730299-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4012950226-r1 { fill: #e1e1e1 } - .terminal-4012950226-r2 { fill: #c5c8c6 } + .terminal-3468730299-r1 { fill: #e1e1e1 } + .terminal-3468730299-r2 { fill: #c5c8c6 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + MyApp - - - - Digits: 0123456789 - ┏━┓ ┓ ╺━┓╺━┓╻ ╻┏━╸┏━╸╺━┓┏━┓┏━┓ - ┃╱┃ ┃ ┏━┛ ━┫┗━┫┗━┓┣━┓  ╹┣━┫┗━┫ - ┗━┛╺┻╸┗━╸╺━┛  ╹╺━┛┗━┛ ╹ ┗━┛╺━┛ - Punctuation:  .+,XYZ^*/-= - ╻ ╻╻ ╻╺━┓ ╻   ╻ - ╺╋╸ ╋  ┳  ▞ ▝ ▘ ✱  ▞ ╺━╸╺━  -  • ▞╹ ╹ ╹ ┗━╸╹  ╺━  - Equation: x = y^2 + 3.14159*y + 10 - ╻ ╻╻ ╻ ╻ ╺━┓╺━┓ ┓ ╻ ╻ ┓ ┏━╸┏━┓╻ ╻ ┓ ┏━┓ -  ╋ ╺━  ┳ ▝ ▘┏━┛╺╋╸ ━┫ ┃ ┗━┫ ┃ ┗━┓┗━┫ ✱  ┳ ╺╋╸ ┃ ┃╱┃ - ╹ ╹╺━  ╹ ┗━╸╺━┛ •╺┻╸  ╹╺┻╸╺━┛╺━┛ ╹ ╺┻╸┗━┛ - - - - - - - - - - - + + + + Digits: 0123456789 + ┏━┓ ┓ ╺━┓╺━┓╻ ╻┏━╸┏━╸╺━┓┏━┓┏━┓ + ┃╱┃ ┃ ┏━┛ ━┫┗━┫┗━┓┣━┓  ╹┣━┫┗━┫ + ┗━┛╺┻╸┗━╸╺━┛  ╹╺━┛┗━┛ ╹ ┗━┛╺━┛ + Punctuation:  .+,XYZ^*/-= + ╻ ╻╻ ╻╺━┓ ╻   ╻ + ╺╋╸ ╋  ┳  ▞ ▝ ▘ ✱  ▞ ╺━╸╺━ +  • ▞╹ ╹ ╹ ┗━╸╹  ╺━ + Equation: x = y^2 + 3.14159*y + 10 + ╻ ╻╻ ╻ ╻ ╺━┓╺━┓ ┓ ╻ ╻ ┓ ┏━╸┏━┓╻ ╻ ┓ ┏━┓ +  ╋ ╺━ ┳ ▝ ▘┏━┛╺╋╸ ━┫ ┃ ┗━┫ ┃ ┗━┓┗━┫ ✱  ┳ ╺╋╸ ┃ ┃╱┃ + ╹ ╹╺━ ╹ ┗━╸╺━┛ •╺┻╸  ╹╺┻╸╺━┛╺━┛ ╹ ╺┻╸┗━┛ + + + + + + + + + + + From a5dbe537e87e18a8d66357b6b95eedeba10d8c32 Mon Sep 17 00:00:00 2001 From: Elias Dorneles Date: Mon, 31 Jul 2023 15:09:38 +0200 Subject: [PATCH 5/5] fix DigitDisplay content width calculation --- src/textual/widgets/_digit_display.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/textual/widgets/_digit_display.py b/src/textual/widgets/_digit_display.py index 671c0efbb7..b725eea73a 100644 --- a/src/textual/widgets/_digit_display.py +++ b/src/textual/widgets/_digit_display.py @@ -209,8 +209,7 @@ class _SingleDigitDisplay(Static): DEFAULT_CSS = """ _SingleDigitDisplay { height: 3; - min-width: 2; - max-width: 3; + width: 3; } """ @@ -244,7 +243,6 @@ def compose(self) -> ComposeResult: disabled=disabled, ) self.digit = initial_value - self._content_width = 3 def validate_digit(self, digit: str) -> str: """Sanitize and validate the digit input.""" @@ -258,11 +256,10 @@ def validate_digit(self, digit: str) -> str: def _watch_digit(self, digit: str) -> None: """Called when the digit attribute changes and passes validation.""" content = _character_map[digit.upper()] - self._content_width = len(content.splitlines()[0]) self.update(content) def get_content_width(self, container: Size, viewport: Size) -> int: - return self._content_width + return 3 def get_content_height(self, container: Size, viewport: Size, width: int) -> int: return 3