diff --git a/changes/3299.feature.rst b/changes/3299.feature.rst new file mode 100644 index 0000000000..8fea0d5b67 --- /dev/null +++ b/changes/3299.feature.rst @@ -0,0 +1 @@ +The web and Textual backends now provide a NumberInput widget. diff --git a/docs/reference/api/widgets/numberinput.rst b/docs/reference/api/widgets/numberinput.rst index 77b532a693..cd5c17a437 100644 --- a/docs/reference/api/widgets/numberinput.rst +++ b/docs/reference/api/widgets/numberinput.rst @@ -35,13 +35,17 @@ A text input that is limited to numeric input. :align: center :width: 300px - .. group-tab:: Web |no| + .. group-tab:: Web - Not supported + .. figure:: /reference/images/numberinput-web.png + :align: center + :width: 300px - .. group-tab:: Textual |no| + .. group-tab:: Textual - Not supported + .. figure:: /reference/images/numberinput-textual.png + :align: center + :width: 300px Usage ----- diff --git a/docs/reference/images/numberinput-textual.png b/docs/reference/images/numberinput-textual.png new file mode 100644 index 0000000000..ef08b7c808 Binary files /dev/null and b/docs/reference/images/numberinput-textual.png differ diff --git a/docs/reference/images/numberinput-web.png b/docs/reference/images/numberinput-web.png new file mode 100644 index 0000000000..b7c3119938 Binary files /dev/null and b/docs/reference/images/numberinput-web.png differ diff --git a/textual/src/toga_textual/factory.py b/textual/src/toga_textual/factory.py index d41f6da7fa..8020cace6b 100644 --- a/textual/src/toga_textual/factory.py +++ b/textual/src/toga_textual/factory.py @@ -23,7 +23,8 @@ from .widgets.label import Label # from .widgets.multilinetextinput import MultilineTextInput -# from .widgets.numberinput import NumberInput +from .widgets.numberinput import NumberInput + # from .widgets.optioncontainer import OptionContainer # from .widgets.passwordinput import PasswordInput # from .widgets.progressbar import ProgressBar @@ -69,7 +70,7 @@ def not_implemented(feature): # "ImageView", "Label", # "MultilineTextInput", - # "NumberInput", + "NumberInput", # "OptionContainer", # "PasswordInput", # "ProgressBar", diff --git a/textual/src/toga_textual/widgets/numberinput.py b/textual/src/toga_textual/widgets/numberinput.py new file mode 100644 index 0000000000..023f0b90cb --- /dev/null +++ b/textual/src/toga_textual/widgets/numberinput.py @@ -0,0 +1,93 @@ +from travertino.size import at_least + +from textual.validation import Number +from textual.widgets import Input as TextualInput + +from .base import Widget + + +class TogaInput(TextualInput): + def __init__(self, impl): + super().__init__() + self.interface = impl.interface + self.impl = impl + + def on_input_changed(self, event: TextualInput.Changed) -> None: + self.interface.on_change() + + def on_input_blurred(self, event: TextualInput.Blurred): + if self.impl.get_value() == "-": + self.impl.set_value("0") + + if self.impl.get_value() is not None and self.impl.min is not None: + if float(self.impl.get_value()) < self.impl.min: + self.impl.set_value(str(self.impl.min)) + + if self.impl.get_value() is not None and self.impl.max is not None: + if float(self.impl.get_value()) > self.impl.max: + self.impl.set_value(str(self.impl.max)) + + +class NumberInput(Widget): + def create(self): + self.native = TogaInput(self) + self.native.type = "number" + self.min = None + self.max = None + + def get_readonly(self): + return self.native.disabled + + def set_readonly(self, value): + self.native.disabled = value + + def get_placeholder(self): + return self.native.placeholder + + def set_placeholder(self, value): + self.native.placeholder = value + + def get_value(self): + if self.native.value == "" or self.native.value is None: + return None + else: + if self.native.value != "-": + return float(self.native.value) + else: + return self.native.value + + def set_value(self, value): + try: + if value is None: + self.native.value = "" + else: + self.native.value = str(value) + except AttributeError: + self.native.value = "" + + def set_step(self, step): + pass + + def set_min_value(self, value): + self.min = value + self.native.validators = [ + Number(minimum=self.min), + ] + + def set_max_value(self, value): + self.max = value + self.native.validators = [ + Number(maximum=self.max), + ] + + @property + def width_adjustment(self): + return 2 + + @property + def height_adjustment(self): + return 2 + + def rehint(self): + self.interface.intrinsic.width = at_least(10) + self.interface.intrinsic.height = 3 diff --git a/web/src/toga_web/factory.py b/web/src/toga_web/factory.py index 3aaa3da4fc..fda40241e8 100644 --- a/web/src/toga_web/factory.py +++ b/web/src/toga_web/factory.py @@ -20,7 +20,8 @@ from .widgets.label import Label # from .widgets.multilinetextinput import MultilineTextInput -# from .widgets.numberinput import NumberInput +from .widgets.numberinput import NumberInput + # from .widgets.optioncontainer import OptionContainer from .widgets.passwordinput import PasswordInput from .widgets.progressbar import ProgressBar @@ -66,7 +67,7 @@ def not_implemented(feature): # 'ImageView', "Label", # 'MultilineTextInput', - # 'NumberInput', + "NumberInput", # 'OptionContainer', "PasswordInput", "ProgressBar", diff --git a/web/src/toga_web/widgets/numberinput.py b/web/src/toga_web/widgets/numberinput.py new file mode 100644 index 0000000000..efb2f00f5a --- /dev/null +++ b/web/src/toga_web/widgets/numberinput.py @@ -0,0 +1,50 @@ +from .base import Widget + + +class NumberInput(Widget): + def create(self): + self._return_listener = None + self.native = self._create_native_widget("sl-input") + self.native.type = "number" + self.native.value = None + self.native.onblur = self.lost_focus + + def lost_focus(self, event): + print(self.native.value == "-") + if self.native.value == "": + self.native.value = None + + if self.native.value is not None and self.native.min is not None: + if float(self.native.value) < self.native.min: + self.native.value = self.native.min + + if self.native.value is not None and self.native.max is not None: + if float(self.native.value) > self.native.max: + self.native.value = self.native.max + + def get_readonly(self, value): + return self.native.readOnly + + def set_readonly(self, value): + self.native.readOnly = value + + def set_step(self, step): + self.native.step = step + + def set_min_value(self, value): + self.native.min = value + + def set_max_value(self, value): + self.native.max = value + + def get_value(self): + if self.native.value == "" or self.native.value is None: + return None + else: + return float(self.native.value) + + def set_value(self, value): + self.native.value = value + + def set_text_align(self, value): + pass