From 01c6704b64c58aa3529ae16e48231f53de3f521c Mon Sep 17 00:00:00 2001 From: kavi2du Date: Sun, 27 Apr 2025 14:00:49 +0800 Subject: [PATCH 1/6] Add ScrollContainer widget to the Toga web backend --- changes/3334.2.feature.rst | 1 + .../api/containers/scrollcontainer.rst | 6 +- docs/reference/data/widgets_by_platform.csv | 2 +- docs/reference/images/scrollcontainer-web.png | Bin 0 -> 8250 bytes web/src/toga_web/factory.py | 4 +- web/src/toga_web/widgets/scrollcontainer.py | 78 ++++++++++++++++++ 6 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 changes/3334.2.feature.rst create mode 100644 docs/reference/images/scrollcontainer-web.png create mode 100644 web/src/toga_web/widgets/scrollcontainer.py diff --git a/changes/3334.2.feature.rst b/changes/3334.2.feature.rst new file mode 100644 index 0000000000..6d9dde8873 --- /dev/null +++ b/changes/3334.2.feature.rst @@ -0,0 +1 @@ +You can now use scroll container on the web – ScrollContainer is supported in Toga Web! diff --git a/docs/reference/api/containers/scrollcontainer.rst b/docs/reference/api/containers/scrollcontainer.rst index 53020130d2..91ab589054 100644 --- a/docs/reference/api/containers/scrollcontainer.rst +++ b/docs/reference/api/containers/scrollcontainer.rst @@ -36,9 +36,11 @@ overflow controlled by scroll bars. :align: center :width: 450px - .. group-tab:: Web |no| + .. group-tab:: Web - Not supported + .. figure:: /reference/images/scrollcontainer-web.png + :align: center + :width: 450px .. group-tab:: Textual |no| diff --git a/docs/reference/data/widgets_by_platform.csv b/docs/reference/data/widgets_by_platform.csv index 4c7e74cda3..23c385c855 100644 --- a/docs/reference/data/widgets_by_platform.csv +++ b/docs/reference/data/widgets_by_platform.csv @@ -26,7 +26,7 @@ Tree,General Widget,:class:`~toga.Tree`,A widget for displaying a hierarchical t WebView,General Widget,:class:`~toga.WebView`,A panel for displaying HTML,|y|,|y|,|y|,|y|,|y|,, Widget,General Widget,:class:`~toga.Widget`,The base widget,|y|,|y|,|y|,|y|,|y|,|b|,|b| Box,Layout Widget,:class:`~toga.Box`,Container for components,|y|,|y|,|y|,|y|,|y|,|b|,|b| -ScrollContainer,Layout Widget,:class:`~toga.ScrollContainer`,A container that can display a layout larger than the area of the container,|y|,|y|,|y|,|y|,|y|,, +ScrollContainer,Layout Widget,:class:`~toga.ScrollContainer`,A container that can display a layout larger than the area of the container,|y|,|y|,|y|,|y|,|y|,|b|, SplitContainer,Layout Widget,:class:`~toga.SplitContainer`,A container that divides an area into two panels with a movable border,|y|,|y|,|y|,,,, OptionContainer,Layout Widget,:class:`~toga.OptionContainer`,A container that can display multiple labeled tabs of content,|y|,|y|,|y|,|y|,|y|,, Camera,Hardware,:class:`~toga.hardware.camera.Camera`,A sensor that can capture photos and/or video.,|y|,,,|y|,|y|,, diff --git a/docs/reference/images/scrollcontainer-web.png b/docs/reference/images/scrollcontainer-web.png new file mode 100644 index 0000000000000000000000000000000000000000..457109a8f3876ef0859441e8f6aa75ce48b502f4 GIT binary patch literal 8250 zcmeAS@N?(olHy`uVBq!ia0y~yVAE${VD98#1B#^DZ)^lojKx9jP7LeL$-D$|SkfJR z9T^xl_H+M9WCij$3p^r=85sBugD~Uq{1qt-4D#naT^vIy;@;lf8~Qmz!Zol`$jH=@ zd0oql9=?ngtvgIpckmmfcbN!o&f%?}zgDs@@%@{f%bGXGZJGJJQVwV+5GehN;)Ag6 zd+vcSg3M0WOPl&${>TaD|4I7qZZ2{B(SxThCdW^PfK&0G;r2;qkj?iy+q2O<4Z>a8cFe=!1{mz00%Qn<;aA^6k^y zV2uxy@>{zfHvct#1PUw$hJtP9nU1FK-|q_b)45Mq4L)AoU;T8pI9UF`?tQ<`c6{{$tdgNP8AT{-Xh42HyR$`^usWk9Khi)njA)x126Ys>Qa@nN_Ifwy?4aH@9?{dk{X3wum0EGlM^zst2|y=aO=G3>*q{cjpvG+M%`=~E z!NLRP?~uAKpx0*l=%_*hnD20JYfCq9))qYfTwo#7cmDs8 zy8ZDWr^h7hzN@Fszz}it^y$+@QFq_IeS326Dd_Zb!&YcaIdoM(!ea?c;MDe-4bl+H s`0Q@*f?0p6{#QZ6?ZezXunFORY%`5cO>CPqUjwAb)78&qol`;+08p<4F8}}l literal 0 HcmV?d00001 diff --git a/web/src/toga_web/factory.py b/web/src/toga_web/factory.py index 3aaa3da4fc..f54af1fd91 100644 --- a/web/src/toga_web/factory.py +++ b/web/src/toga_web/factory.py @@ -24,8 +24,8 @@ # from .widgets.optioncontainer import OptionContainer from .widgets.passwordinput import PasswordInput from .widgets.progressbar import ProgressBar +from .widgets.scrollcontainer import ScrollContainer -# from .widgets.scrollcontainer import ScrollContainer # from .widgets.selection import Selection # from .widgets.slider import Slider # from .widgets.splitcontainer import SplitContainer @@ -71,7 +71,7 @@ def not_implemented(feature): "PasswordInput", "ProgressBar", "ActivityIndicator", - # 'ScrollContainer', + "ScrollContainer", # 'Selection', # 'Slider', # 'SplitContainer', diff --git a/web/src/toga_web/widgets/scrollcontainer.py b/web/src/toga_web/widgets/scrollcontainer.py new file mode 100644 index 0000000000..396303bc0e --- /dev/null +++ b/web/src/toga_web/widgets/scrollcontainer.py @@ -0,0 +1,78 @@ +from .base import Widget + + +class ScrollContainer(Widget): + def __init__(self, interface): + super().__init__(interface) + self.native.addEventListener("scroll", self._on_native_scroll) + + def create(self): + self._horizontal_enabled = True + self._vertical_enabled = True + + self.native = self._create_native_widget( + "div", + classes=["toga-scroll-container"], + ) + self._content_div = self._create_native_widget( + "div", + classes=["toga-scroll-content"], + ) + self.native.appendChild(self._content_div) + self._update_overflow() + + def set_content(self, content_impl): + while self._content_div.firstChild: + self._content_div.removeChild(self._content_div.firstChild) + + if content_impl is not None: + self._content_div.appendChild(content_impl.native) + + def _update_overflow(self): + self.native.style.overflowX = "auto" if self._horizontal_enabled else "hidden" + self.native.style.overflowY = "auto" if self._vertical_enabled else "hidden" + + def get_horizontal(self): + return self._horizontal_enabled + + def set_horizontal(self, value): + self._horizontal_enabled = bool(value) + self._update_overflow() + + def get_vertical(self): + return self._vertical_enabled + + def set_vertical(self, value): + self._vertical_enabled = bool(value) + self._update_overflow() + + def get_max_horizontal_position(self): + return max(0, self.native.scrollWidth - self.native.clientWidth) + + def get_max_vertical_position(self): + return max(0, self.native.scrollHeight - self.native.clientHeight) + + def get_horizontal_position(self): + return self.native.scrollLeft + + def get_vertical_position(self): + return self.native.scrollTop + + def set_position(self, horizontal, vertical): + if self._horizontal_enabled: + horizontal = max( + 0, min(int(horizontal), self.get_max_horizontal_position()) + ) + else: + horizontal = self.native.scrollLeft + + if self._vertical_enabled: + vertical = max(0, min(int(vertical), self.get_max_vertical_position())) + else: + vertical = self.native.scrollTop + + self.native.scrollTo(horizontal, vertical) + + def _on_native_scroll(self, _evt): + if self.interface.on_scroll: + self.interface.on_scroll(widget=self.interface) From 0f88117a95d00c71f8ed44cb0ff534a82142a8fa Mon Sep 17 00:00:00 2001 From: kavi2du Date: Sun, 27 Apr 2025 15:37:49 +0800 Subject: [PATCH 2/6] Minor change --- changes/3334.2.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/3334.2.feature.rst b/changes/3334.2.feature.rst index 6d9dde8873..1e8ac5c5bb 100644 --- a/changes/3334.2.feature.rst +++ b/changes/3334.2.feature.rst @@ -1 +1 @@ -You can now use scroll container on the web – ScrollContainer is supported in Toga Web! +You can now use scroll container on the web — ScrollContainer is supported in Toga web! From d56fb3a900a03c2091a755a49c57459f45b59a81 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Tue, 29 Apr 2025 09:59:25 +0800 Subject: [PATCH 3/6] Address review comments --- changes/3334.2.feature.rst | 2 +- web/src/toga_web/widgets/scrollcontainer.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/changes/3334.2.feature.rst b/changes/3334.2.feature.rst index 1e8ac5c5bb..3a18c19852 100644 --- a/changes/3334.2.feature.rst +++ b/changes/3334.2.feature.rst @@ -1 +1 @@ -You can now use scroll container on the web — ScrollContainer is supported in Toga web! +The web backend now supports the ScrollContainer widget. diff --git a/web/src/toga_web/widgets/scrollcontainer.py b/web/src/toga_web/widgets/scrollcontainer.py index 396303bc0e..ea2116be0e 100644 --- a/web/src/toga_web/widgets/scrollcontainer.py +++ b/web/src/toga_web/widgets/scrollcontainer.py @@ -4,7 +4,7 @@ class ScrollContainer(Widget): def __init__(self, interface): super().__init__(interface) - self.native.addEventListener("scroll", self._on_native_scroll) + self.native.addEventListener("scroll", self.dom_onscroll) def create(self): self._horizontal_enabled = True @@ -12,7 +12,7 @@ def create(self): self.native = self._create_native_widget( "div", - classes=["toga-scroll-container"], + classes=["container"], ) self._content_div = self._create_native_widget( "div", @@ -73,6 +73,6 @@ def set_position(self, horizontal, vertical): self.native.scrollTo(horizontal, vertical) - def _on_native_scroll(self, _evt): + def dom_onscroll(self, _evt): if self.interface.on_scroll: self.interface.on_scroll(widget=self.interface) From 4c93e7a3800290be3d2a5df083726460c6325b43 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 2 May 2025 11:52:05 +0800 Subject: [PATCH 4/6] Use a proxy for scroll events. --- web/src/toga_web/widgets/scrollcontainer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/toga_web/widgets/scrollcontainer.py b/web/src/toga_web/widgets/scrollcontainer.py index ea2116be0e..a2eada1f7b 100644 --- a/web/src/toga_web/widgets/scrollcontainer.py +++ b/web/src/toga_web/widgets/scrollcontainer.py @@ -1,10 +1,12 @@ +from toga_web.libs import create_proxy + from .base import Widget class ScrollContainer(Widget): def __init__(self, interface): super().__init__(interface) - self.native.addEventListener("scroll", self.dom_onscroll) + self.native.addEventListener("scroll", create_proxy(self.dom_onscroll)) def create(self): self._horizontal_enabled = True From c7ca1df0fdc7add8b35e8943e6d2c37b16a0d8ce Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 2 May 2025 11:52:36 +0800 Subject: [PATCH 5/6] Simplify (and correct usage) of event propegation. --- web/src/toga_web/widgets/scrollcontainer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/toga_web/widgets/scrollcontainer.py b/web/src/toga_web/widgets/scrollcontainer.py index a2eada1f7b..d5f362274c 100644 --- a/web/src/toga_web/widgets/scrollcontainer.py +++ b/web/src/toga_web/widgets/scrollcontainer.py @@ -76,5 +76,4 @@ def set_position(self, horizontal, vertical): self.native.scrollTo(horizontal, vertical) def dom_onscroll(self, _evt): - if self.interface.on_scroll: - self.interface.on_scroll(widget=self.interface) + self.interface.on_scroll() From fef4b4f49b3635036fb3cd07d9d000c296a90877 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 2 May 2025 12:06:37 +0800 Subject: [PATCH 6/6] Correct some proxy use on other widgets. --- web/src/toga_web/widgets/button.py | 4 +++- web/src/toga_web/widgets/passwordinput.py | 6 ------ web/src/toga_web/widgets/textinput.py | 14 ++++++++++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/web/src/toga_web/widgets/button.py b/web/src/toga_web/widgets/button.py index d1b177c0a5..73632a41f6 100644 --- a/web/src/toga_web/widgets/button.py +++ b/web/src/toga_web/widgets/button.py @@ -1,10 +1,12 @@ +from toga_web.libs import create_proxy + from .base import Widget class Button(Widget): def create(self): self.native = self._create_native_widget("sl-button") - self.native.onclick = self.dom_onclick + self.native.addEventListener("onclick", create_proxy(self.dom_onclick)) def dom_onclick(self, event): self.interface.on_press() diff --git a/web/src/toga_web/widgets/passwordinput.py b/web/src/toga_web/widgets/passwordinput.py index 76b1064e38..fbc96f7571 100644 --- a/web/src/toga_web/widgets/passwordinput.py +++ b/web/src/toga_web/widgets/passwordinput.py @@ -1,5 +1,3 @@ -from toga_web.libs import create_proxy - from .textinput import TextInput @@ -7,10 +5,6 @@ class PasswordInput(TextInput): def create(self): super().create() self.native.setAttribute("type", "password") - self.native.addEventListener("sl-change", create_proxy(self.dom_onchange)) - - def dom_onchange(self, event): - self.interface.on_change(None) def is_valid(self): self.interface.factory.not_implemented("PasswordInput.is_valid()") diff --git a/web/src/toga_web/widgets/textinput.py b/web/src/toga_web/widgets/textinput.py index fa30b017fc..1760983208 100644 --- a/web/src/toga_web/widgets/textinput.py +++ b/web/src/toga_web/widgets/textinput.py @@ -1,3 +1,5 @@ +from toga_web.libs import create_proxy + from .base import Widget @@ -5,11 +7,15 @@ class TextInput(Widget): def create(self): self._return_listener = None self.native = self._create_native_widget("sl-input") - self.native.onkeyup = self.dom_keyup + self.native.onkeyup = self.dom_onkeyup + self.native.addEventListener("onkeyup", create_proxy(self.dom_onkeyup)) + self.native.addEventListener("sl-change", create_proxy(self.dom_sl_change)) + + def dom_onkeyup(self, event): + self.interface.on_change() - def dom_keyup(self, event): - if event.key == "Enter": - self.interface.on_confirm() + def dom_sl_change(self, event): + self.interface.on_confirm() def set_readonly(self, value): self.native.readOnly = value