diff --git a/changes/3334.feature.rst b/changes/3334.feature.rst new file mode 100644 index 0000000000..c5a4b124ae --- /dev/null +++ b/changes/3334.feature.rst @@ -0,0 +1 @@ +The Toga Web backend now supports the MultilineTextInput widget. diff --git a/docs/en/reference/api/widgets/multilinetextinput.md b/docs/en/reference/api/widgets/multilinetextinput.md index 44568fb6d5..9e531f986d 100644 --- a/docs/en/reference/api/widgets/multilinetextinput.md +++ b/docs/en/reference/api/widgets/multilinetextinput.md @@ -62,9 +62,15 @@ A scrollable panel that allows for the display and editing of multiple lines of /// -/// tab | Web {{ not_supported }} +/// tab | Web -Not supported +![/reference/images/multilinetextinput-web.png](/reference/images/multilinetextinput-web.png){ width="300" } + +/// caption + +/// + + /// diff --git a/docs/en/reference/data/widgets_by_platform.csv b/docs/en/reference/data/widgets_by_platform.csv index d9964bc034..86725e5dec 100644 --- a/docs/en/reference/data/widgets_by_platform.csv +++ b/docs/en/reference/data/widgets_by_platform.csv @@ -12,7 +12,7 @@ Divider,General Widget,[`Divider`][toga.Divider],A horizontal or vertical line, ImageView,General Widget,[`ImageView`][toga.ImageView],A widget that displays an image,●,●,●,●,●,, Label,General Widget,[`Label`][toga.Label],Text label,●,●,●,●,●,○,○ MapView,General Widget,[`MapView`][toga.MapView],A zoomable map that can be annotated with location pins,●,●,●,●,●,, -MultilineTextInput,General Widget,[`MultilineTextInput`][toga.MultilineTextInput],Multi-line Text Input field,●,●,●,●,●,, +MultilineTextInput,General Widget,[`MultilineTextInput`][toga.MultilineTextInput],Multi-line Text Input field,●,●,●,●,●,○, NumberInput,General Widget,[`NumberInput`][toga.NumberInput],A text input that is limited to numeric input,●,●,●,●,●,, PasswordInput,General Widget,[`PasswordInput`][toga.PasswordInput],A text input that hides its input,●,●,●,●,●,○, ProgressBar,General Widget,[`ProgressBar`][toga.ProgressBar],Progress Bar,●,●,●,●,●,○, diff --git a/docs/en/reference/images/multilinetextinput-web.png b/docs/en/reference/images/multilinetextinput-web.png new file mode 100644 index 0000000000..410ac2e4e4 Binary files /dev/null and b/docs/en/reference/images/multilinetextinput-web.png differ diff --git a/web/src/toga_web/factory.py b/web/src/toga_web/factory.py index 17b0d1fab1..78831a82aa 100644 --- a/web/src/toga_web/factory.py +++ b/web/src/toga_web/factory.py @@ -19,8 +19,8 @@ # from .widgets.detailedlist import DetailedList # from .widgets.imageview import ImageView from .widgets.label import Label +from .widgets.multilinetextinput import MultilineTextInput -# from .widgets.multilinetextinput import MultilineTextInput # from .widgets.numberinput import NumberInput # from .widgets.optioncontainer import OptionContainer from .widgets.passwordinput import PasswordInput @@ -69,7 +69,7 @@ def not_implemented(feature): # 'DetailedList', # 'ImageView', "Label", - # 'MultilineTextInput', + "MultilineTextInput", # 'NumberInput', # 'OptionContainer', "PasswordInput", diff --git a/web/src/toga_web/widgets/multilinetextinput.py b/web/src/toga_web/widgets/multilinetextinput.py new file mode 100644 index 0000000000..50acf23d28 --- /dev/null +++ b/web/src/toga_web/widgets/multilinetextinput.py @@ -0,0 +1,113 @@ +from travertino.colors import TRANSPARENT +from travertino.size import at_least + +from toga_web.libs import create_proxy + +from .base import Widget + + +class MultilineTextInput(Widget): + def create(self): + self.native = self._create_native_widget("sl-textarea") + self.native.addEventListener("sl-input", create_proxy(self._on_input)) + + def _after_render(_): + self.native.shadowRoot.querySelector("textarea").style.resize = "none" + + self.native.updateComplete.then(create_proxy(_after_render)) + + def _on_input(self, event): + self.interface.on_change() + + def get_value(self): + return self.native.value or "" + + def set_value(self, value): + self.native.value = "" if value is None else str(value) + self.interface.on_change() + + def get_placeholder(self): + return self.native.placeholder or "" + + def set_placeholder(self, value): + self.native.placeholder = value or "" + + def get_readonly(self): + return bool(self.native.readonly) + + def set_readonly(self, value): + self.native.readonly = bool(value) + + def get_enabled(self): + return not bool(self.native.disabled) + + def set_enabled(self, value): + self.native.disabled = not bool(value) + + def _to_css_color(self, value: object) -> str: + if value is None or value is TRANSPARENT: + return "" + try: + return str(value) + except Exception: + return "" + + def set_background_color(self, color): + css = self._to_css_color(color) + + def _apply(_): + inner = self.native.shadowRoot.querySelector("textarea") + if inner: + if css: + inner.style.setProperty("background", css) + else: + inner.style.removeProperty("background") + + self.native.updateComplete.then(create_proxy(_apply)) + + def set_color(self, color): + css = self._to_css_color(color) + + def _apply(_): + inner = self.native.shadowRoot.querySelector("textarea") + if inner: + if css: + inner.style.setProperty("color", css) + else: + inner.style.removeProperty("color") + + self.native.updateComplete.then(create_proxy(_apply)) + + def set_text_align(self, value): + mapping = {0: "left", 1: "right", 2: "center", 3: "justify"} + css_align = mapping.get(value, value if isinstance(value, str) else "left") + + def _apply(_): + inner = self.native.shadowRoot.querySelector("textarea") + if inner: + inner.style.textAlign = css_align + + self.native.updateComplete.then(create_proxy(_apply)) + + def set_font(self, font): + pass + + def rehint(self): + self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH) + self.interface.intrinsic.height = at_least(self.interface._MIN_HEIGHT) + + def scroll_to_top(self): + def _go(_): + inner = self.native.shadowRoot.querySelector("textarea") + if inner: + inner.scrollTop = 0 + + self.native.updateComplete.then(create_proxy(_go)) + + def scroll_to_bottom(self): + def _go(_): + inner = self.native.shadowRoot.querySelector("textarea") + if inner: + inner.scrollTop = max(0, inner.scrollHeight - inner.clientHeight) + + self.native.updateComplete.then(create_proxy(_go))