diff --git a/changes/4227.feature.md b/changes/4227.feature.md new file mode 100644 index 0000000000..23506498f5 --- /dev/null +++ b/changes/4227.feature.md @@ -0,0 +1 @@ +The web backend now uses WebAwesome instead of Shoelace to provide web components. diff --git a/core/src/toga/sources/base.py b/core/src/toga/sources/base.py index 08796e671a..bd7509e516 100644 --- a/core/src/toga/sources/base.py +++ b/core/src/toga/sources/base.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from typing import Generic, Protocol, TypeVar, runtime_checkable ListenerT = TypeVar("ListenerT") @@ -128,8 +129,6 @@ def notify(self, notification: str, **kwargs: object) -> None: if method is None: method = getattr(listener, notification, None) if method is not None: - import warnings - warnings.warn( f"Notification handler methods on Listeners now start with " f"'source_'. Change the method name to " diff --git a/docs/en/how-to/contribute/what/implement-feature.md b/docs/en/how-to/contribute/what/implement-feature.md index 7fd50e9267..63aa49057d 100644 --- a/docs/en/how-to/contribute/what/implement-feature.md +++ b/docs/en/how-to/contribute/what/implement-feature.md @@ -10,7 +10,7 @@ Don't worry if you don't have an idea for a new feature, there are [plenty of fe ### Implement a platform native widget -If the core library already specifies an interface for a widget, but the widget isn't implemented on your platform of choice, implement that interface. The [API reference table](/reference/api/index.md) table can show you the widgets that are missing on various platforms. You can also look for log messages in a running app (or the direct `factory.not_implemented()` function calls that produce those log messages). At present, Qt, the Web and Textual backends have the most missing widgets. If you have web skills, or would like to learn more about [PyScript](https://pyscript.net) and [Shoelace](https://shoelace.style), the web backend could be a good place to contribute; if you'd like to learn more about terminal applications or the [Textual](https://textual.textualize.io) API, contributing to the Textual backend could be a good place for you to contribute. If you’re interested in desktop GUI development or want to deepen your understanding of the Qt framework, contributing to the [Qt](https://www.qt.io/product/framework) backend is a great option. +If the core library already specifies an interface for a widget, but the widget isn't implemented on your platform of choice, implement that interface. The [API reference table](/reference/api/index.md) table can show you the widgets that are missing on various platforms. You can also look for log messages in a running app (or the direct `factory.not_implemented()` function calls that produce those log messages). At present, Qt, the Web and Textual backends have the most missing widgets. If you have web skills, or would like to learn more about [PyScript](https://pyscript.net) and [WebAwesome](https://www.webawesome.com), the web backend could be a good place to contribute; if you'd like to learn more about terminal applications or the [Textual](https://textual.textualize.io) API, contributing to the Textual backend could be a good place for you to contribute. If you’re interested in desktop GUI development or want to deepen your understanding of the Qt framework, contributing to the [Qt](https://www.qt.io/product/framework) backend is a great option. Alternatively, if there's a widget that doesn't exist, propose an interface design, and implement it for at least one platform. You may find [this presentation by BeeWare emeritus team member Dan Yeaw](https://www.youtube.com/watch?v=sWt_sEZUiY8) helpful. This talk gives an architectural overview of Toga, as well as providing a guide to the process of adding new widgets. diff --git a/docs/en/reference/platforms/web.md b/docs/en/reference/platforms/web.md index 6897d1910d..3a32363e81 100644 --- a/docs/en/reference/platforms/web.md +++ b/docs/en/reference/platforms/web.md @@ -10,7 +10,7 @@ The Web backend is currently proof-of-concept only. Most widgets have not been i ## Prerequisites -`toga-web` will run in any modern browser. It requires [PyScript](https://pyscript.net) 2023.05.01 or newer, and [Shoelace v2.3](https://shoelace.style). +`toga-web` will run in any modern browser. It requires [PyScript](https://pyscript.net) 2023.05.01 or newer, and [WebAwesome v3.3](https://www.webawesome.com). ## Installation @@ -20,6 +20,6 @@ The recommended approach for deploying `toga-web` is to use [Briefcase](https:// ## Implementation details -The `toga-web` backend is implemented using [Shoelace web components](https://shoelace.style). +The `toga-web` backend is implemented using [WebAwesome web components](https://www.webawesome.com). The DOM is accessed using [PyScript](https://pyscript.net). diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 2e3b317f41..3533ced270 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -230,6 +230,7 @@ viewport watchOS Wayland WebKit +WebAwesome WebView WebViews WGS diff --git a/examples/activityindicator/pyproject.toml b/examples/activityindicator/pyproject.toml index 20df77c913..f82b8c49d7 100644 --- a/examples/activityindicator/pyproject.toml +++ b/examples/activityindicator/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/beeliza/pyproject.toml b/examples/beeliza/pyproject.toml index 660bf77bab..7a3997f9de 100644 --- a/examples/beeliza/pyproject.toml +++ b/examples/beeliza/pyproject.toml @@ -63,4 +63,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/box/pyproject.toml b/examples/box/pyproject.toml index 33e8444084..8764ca06a2 100644 --- a/examples/box/pyproject.toml +++ b/examples/box/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/button/pyproject.toml b/examples/button/pyproject.toml index f3d84016d5..6364fefc52 100644 --- a/examples/button/pyproject.toml +++ b/examples/button/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/canvas/pyproject.toml b/examples/canvas/pyproject.toml index e30db33767..773cf47036 100644 --- a/examples/canvas/pyproject.toml +++ b/examples/canvas/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/colors/pyproject.toml b/examples/colors/pyproject.toml index 2648afa409..e4b6624ef7 100644 --- a/examples/colors/pyproject.toml +++ b/examples/colors/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/command/pyproject.toml b/examples/command/pyproject.toml index c038212a66..1b14422af6 100644 --- a/examples/command/pyproject.toml +++ b/examples/command/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/date_and_time/pyproject.toml b/examples/date_and_time/pyproject.toml index 9be0da8e51..07f1c354ff 100644 --- a/examples/date_and_time/pyproject.toml +++ b/examples/date_and_time/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/detailedlist/pyproject.toml b/examples/detailedlist/pyproject.toml index ab60bdaed7..1937edcc07 100644 --- a/examples/detailedlist/pyproject.toml +++ b/examples/detailedlist/pyproject.toml @@ -63,4 +63,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/dialogs/pyproject.toml b/examples/dialogs/pyproject.toml index 8e56caefa1..156ab87707 100644 --- a/examples/dialogs/pyproject.toml +++ b/examples/dialogs/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/divider/pyproject.toml b/examples/divider/pyproject.toml index cd38047ea0..10695c8026 100644 --- a/examples/divider/pyproject.toml +++ b/examples/divider/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/examples_overview/pyproject.toml b/examples/examples_overview/pyproject.toml index 71f7eb2c76..ef38d7015c 100644 --- a/examples/examples_overview/pyproject.toml +++ b/examples/examples_overview/pyproject.toml @@ -63,4 +63,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/focus/pyproject.toml b/examples/focus/pyproject.toml index b300e43b02..0017c5b125 100644 --- a/examples/focus/pyproject.toml +++ b/examples/focus/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/font/pyproject.toml b/examples/font/pyproject.toml index daefdae20d..6f700e0bc5 100644 --- a/examples/font/pyproject.toml +++ b/examples/font/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/font_size/pyproject.toml b/examples/font_size/pyproject.toml index 8acb5e46b5..1a3e9dd001 100644 --- a/examples/font_size/pyproject.toml +++ b/examples/font_size/pyproject.toml @@ -61,4 +61,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/handlers/pyproject.toml b/examples/handlers/pyproject.toml index ac8d7af3ef..df896ecb57 100644 --- a/examples/handlers/pyproject.toml +++ b/examples/handlers/pyproject.toml @@ -63,4 +63,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/imageview/pyproject.toml b/examples/imageview/pyproject.toml index 02fb3761a5..7269c11b3c 100644 --- a/examples/imageview/pyproject.toml +++ b/examples/imageview/pyproject.toml @@ -67,4 +67,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/layout/pyproject.toml b/examples/layout/pyproject.toml index affcd1de32..984ebb9ed2 100644 --- a/examples/layout/pyproject.toml +++ b/examples/layout/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/mapview/pyproject.toml b/examples/mapview/pyproject.toml index b58032e715..efa6feb25e 100644 --- a/examples/mapview/pyproject.toml +++ b/examples/mapview/pyproject.toml @@ -63,4 +63,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/multilinetextinput/pyproject.toml b/examples/multilinetextinput/pyproject.toml index 9fd51a19b0..fd243c317c 100644 --- a/examples/multilinetextinput/pyproject.toml +++ b/examples/multilinetextinput/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/numberinput/pyproject.toml b/examples/numberinput/pyproject.toml index 00bfb92880..f576b13b59 100644 --- a/examples/numberinput/pyproject.toml +++ b/examples/numberinput/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/optioncontainer/pyproject.toml b/examples/optioncontainer/pyproject.toml index 6557cee93b..9efe87d3ce 100644 --- a/examples/optioncontainer/pyproject.toml +++ b/examples/optioncontainer/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/passwordinput/LICENSE b/examples/passwordinput/LICENSE new file mode 100644 index 0000000000..f09583bfab --- /dev/null +++ b/examples/passwordinput/LICENSE @@ -0,0 +1 @@ +Released under the same license as Toga. See the root of the Toga repository for details. diff --git a/examples/passwordinput/pyproject.toml b/examples/passwordinput/pyproject.toml index 12694699ba..e9d5590848 100644 --- a/examples/passwordinput/pyproject.toml +++ b/examples/passwordinput/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ '../../web', ] -style_framework = "Shoelace v2.3" diff --git a/examples/positron-django/pyproject.toml b/examples/positron-django/pyproject.toml index 885f350f6c..62c25218ff 100644 --- a/examples/positron-django/pyproject.toml +++ b/examples/positron-django/pyproject.toml @@ -64,4 +64,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/positron-static/pyproject.toml b/examples/positron-static/pyproject.toml index 30fdfab242..646c31ea49 100644 --- a/examples/positron-static/pyproject.toml +++ b/examples/positron-static/pyproject.toml @@ -63,4 +63,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/progressbar/pyproject.toml b/examples/progressbar/pyproject.toml index 7b2ac0fd8e..e9617f4438 100644 --- a/examples/progressbar/pyproject.toml +++ b/examples/progressbar/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/resize/pyproject.toml b/examples/resize/pyproject.toml index 79a12637d4..314118f4b9 100644 --- a/examples/resize/pyproject.toml +++ b/examples/resize/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/scrollcontainer/pyproject.toml b/examples/scrollcontainer/pyproject.toml index bd453e3ece..f96bdc6eab 100644 --- a/examples/scrollcontainer/pyproject.toml +++ b/examples/scrollcontainer/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/selection/pyproject.toml b/examples/selection/pyproject.toml index 09000f207f..66bf346344 100644 --- a/examples/selection/pyproject.toml +++ b/examples/selection/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/simpleapp/pyproject.toml b/examples/simpleapp/pyproject.toml index 811ae7066e..5d69ef7ae2 100644 --- a/examples/simpleapp/pyproject.toml +++ b/examples/simpleapp/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/slider/pyproject.toml b/examples/slider/pyproject.toml index dc2bd6a075..840dff072e 100644 --- a/examples/slider/pyproject.toml +++ b/examples/slider/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/splitcontainer/pyproject.toml b/examples/splitcontainer/pyproject.toml index 6d8593e581..b100b9d722 100644 --- a/examples/splitcontainer/pyproject.toml +++ b/examples/splitcontainer/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/switch_demo/pyproject.toml b/examples/switch_demo/pyproject.toml index 5464e582eb..0a234fcc08 100644 --- a/examples/switch_demo/pyproject.toml +++ b/examples/switch_demo/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/table/pyproject.toml b/examples/table/pyproject.toml index 9b29b34093..340cb25547 100644 --- a/examples/table/pyproject.toml +++ b/examples/table/pyproject.toml @@ -61,4 +61,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/table_columns/pyproject.toml b/examples/table_columns/pyproject.toml index 629306d972..71d8537b53 100644 --- a/examples/table_columns/pyproject.toml +++ b/examples/table_columns/pyproject.toml @@ -65,4 +65,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/table_source/pyproject.toml b/examples/table_source/pyproject.toml index 57d32e6ad9..1838c1ed54 100644 --- a/examples/table_source/pyproject.toml +++ b/examples/table_source/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/textinput/pyproject.toml b/examples/textinput/pyproject.toml index 88b18555b5..8f5d6d5173 100644 --- a/examples/textinput/pyproject.toml +++ b/examples/textinput/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/tree/pyproject.toml b/examples/tree/pyproject.toml index 08ed3f3285..383bf5a736 100644 --- a/examples/tree/pyproject.toml +++ b/examples/tree/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/tree_source/pyproject.toml b/examples/tree_source/pyproject.toml index e4a99cb9c5..443347aef4 100644 --- a/examples/tree_source/pyproject.toml +++ b/examples/tree_source/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/tutorial0/pyproject.toml b/examples/tutorial0/pyproject.toml index 19825aacf5..d985ad9406 100644 --- a/examples/tutorial0/pyproject.toml +++ b/examples/tutorial0/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/tutorial0/sandbox.html b/examples/tutorial0/sandbox.html deleted file mode 100644 index 70f1cce6c4..0000000000 --- a/examples/tutorial0/sandbox.html +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - First App - - - - - - - - - - - - - -
- First App logo - - -
-
-
- Hello world -
-
- - diff --git a/examples/tutorial1/pyproject.toml b/examples/tutorial1/pyproject.toml index 91776ca945..5ee9e8d49d 100644 --- a/examples/tutorial1/pyproject.toml +++ b/examples/tutorial1/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/tutorial2/pyproject.toml b/examples/tutorial2/pyproject.toml index 487bb2188d..bf7130de38 100644 --- a/examples/tutorial2/pyproject.toml +++ b/examples/tutorial2/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/tutorial3/pyproject.toml b/examples/tutorial3/pyproject.toml index 949d1882f7..0ccb8c4ec6 100644 --- a/examples/tutorial3/pyproject.toml +++ b/examples/tutorial3/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/tutorial4/pyproject.toml b/examples/tutorial4/pyproject.toml index a9f738bc0e..8e29ca58fb 100644 --- a/examples/tutorial4/pyproject.toml +++ b/examples/tutorial4/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/webview/pyproject.toml b/examples/webview/pyproject.toml index b66dde4faa..b8a070bdb3 100644 --- a/examples/webview/pyproject.toml +++ b/examples/webview/pyproject.toml @@ -66,4 +66,3 @@ chaquopy.defaultConfig.staticProxy("toga_android.widgets.internal.webview") requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/examples/window/pyproject.toml b/examples/window/pyproject.toml index bb85778178..dade244077 100644 --- a/examples/window/pyproject.toml +++ b/examples/window/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" diff --git a/web/README.md b/web/README.md index 111c8209e9..e3795cfa68 100644 --- a/web/README.md +++ b/web/README.md @@ -6,7 +6,7 @@ [![Project status](https://img.shields.io/pypi/status/toga-web.svg)](https://pypi.python.org/pypi/toga-web) -A backend for the [Toga widget toolkit](https://beeware.org/toga) on web platforms. It uses [Shoelace](https://shoelace.style) to provide web components. +A backend for the [Toga widget toolkit](https://beeware.org/toga) on web platforms. It uses [WebAwesome](https://www.webawesome.com) to provide web components. This package isn't much use by itself; it needs to be combined with [the core Toga library](https://pypi.python.org/pypi/toga-core). diff --git a/web/pyproject.toml b/web/pyproject.toml index b88cb9a213..267fb920f5 100644 --- a/web/pyproject.toml +++ b/web/pyproject.toml @@ -80,7 +80,7 @@ SimpleStatusIcon = "toga_web.statusicons:SimpleStatusIcon" StatusIconSet = "toga_web.statusicons:StatusIconSet" # Widgets -# ActivityIndicator = "toga_web.widgets.activityindicator:ActivityIndicator" +ActivityIndicator = "toga_web.widgets.activityindicator:ActivityIndicator" Box = "toga_web.widgets.box:Box" Button = "toga_web.widgets.button:Button" Canvas = "toga_web.widgets.canvas:Canvas" @@ -102,7 +102,7 @@ Slider = "toga_web.widgets.slider:Slider" Switch = "toga_web.widgets.switch:Switch" # Table = "toga_web.widgets.table:Table" TextInput = "toga_web.widgets.textinput:TextInput" -# TimeInput = "toga_web.widgets.timeinput:TimeInput" +TimeInput = "toga_web.widgets.timeinput:TimeInput" # Tree = "toga_web.widgets.tree:Tree" # WebView = "toga_web.widgets.webview:WebView" diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index 49a1f7e547..46d9654158 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -95,10 +95,10 @@ def show_about_dialog(self): copyright = f"\n\nCopyright © {self.interface.author}" close_button = create_element( - "sl-button", slot="footer", variant="primary", content="Ok" + "wa-button", slot="footer", variant="brand", content="Ok" ) about_dialog = create_element( - "sl-dialog", + "wa-dialog", id="toga-about-dialog", label="About", children=[ @@ -111,7 +111,7 @@ def show_about_dialog(self): # Create a button handler to capture the close, # and destroy the dialog def dialog_close(event): - about_dialog.hide() + about_dialog.open = False self.native.removeChild(about_dialog) close_button.onclick = dialog_close @@ -119,15 +119,15 @@ def dialog_close(event): # Add the dialog to the DOM. self.native.appendChild(about_dialog) - # If this is the first time a dialog is being shown, the Shoelace + # If this is the first time a dialog is being shown, the WebAwesome # autoloader needs to construct the Dialog custom element. We can't # display the dialog until that element has been fully loaded and - # constructed. Only show the dialog when the promise of + # constructed. Only show the dialog when the promise of # element construction has been fulfilled. def show_dialog(promise): - about_dialog.show() + about_dialog.open = True - js.customElements.whenDefined("sl-dialog").then(show_dialog) + js.customElements.whenDefined("wa-dialog").then(show_dialog) ###################################################################### # Cursor control diff --git a/web/src/toga_web/deploy/inserts/index.html~head b/web/src/toga_web/deploy/inserts/index.html~head index 9131aa3ac3..aeb9605543 100644 --- a/web/src/toga_web/deploy/inserts/index.html~head +++ b/web/src/toga_web/deploy/inserts/index.html~head @@ -1,3 +1,5 @@ - - - + + + + + diff --git a/web/src/toga_web/dialogs.py b/web/src/toga_web/dialogs.py index 45b8e29dc5..ff8bda48cd 100644 --- a/web/src/toga_web/dialogs.py +++ b/web/src/toga_web/dialogs.py @@ -11,7 +11,7 @@ def show(self, host_window, future): # modal dialogs. toga.App.app._impl.native.appendChild(self.native) - self.native.show() + self.native.open = True else: # Dialog doesn't have an implementation self.future.set_result(None) @@ -21,7 +21,7 @@ class InfoDialog(BaseDialog): def __init__(self, title, message): super().__init__() self.native = create_element( - "sl-dialog", + "wa-dialog", id="toga-info-dialog", label=title, children=[ @@ -32,7 +32,7 @@ def __init__(self, title, message): def create_buttons(self): close_button = create_element( - "sl-button", slot="footer", variant="primary", content="Ok" + "wa-button", slot="footer", variant="brand", content="Ok" ) # Handle the close of the dialog close_button.onclick = self.dialog_close @@ -40,7 +40,7 @@ def create_buttons(self): return [close_button] def dialog_close(self, event): - self.native.hide() + self.native.open = False self.native.parentElement.removeChild(self.native) self.future.set_result(None) diff --git a/web/src/toga_web/static/toga.css b/web/src/toga_web/static/toga.css index e3aa84d253..05d5404549 100644 --- a/web/src/toga_web/static/toga.css +++ b/web/src/toga_web/static/toga.css @@ -13,8 +13,8 @@ html, body { } #app-placeholder { - font-family: var(--sl-font-sans); - font-size: var(--sl-font-size-medium); + font-family: var(--wa-font-family-body); + font-size: var(--wa-font-size-m); } /* If a custom element hasn't been defined yet, hide it from rendering */ @@ -27,8 +27,8 @@ html, body { **********************************************************************/ header.toga { - background-color: var(--sl-color-primary-800); - color: var(--sl-color-neutral-50); + background-color: var(--wa-color-brand-40); + color: var(--wa-color-brand-on-loud); display: flex; flex-direction: row; align-items: center; @@ -50,8 +50,8 @@ header.toga nav { header.toga nav.menubar { flex-grow: 1; - font-family: var(--sl-font-sans); - font-size: var(--sl-font-size-large); + font-family: var(--wa-font-family-body); + font-size: var(--wa-font-size-l); } header.toga nav.menubar:last-of-type { @@ -62,18 +62,18 @@ header.toga nav.menubar:last-of-type { /***** Menubar ********************************************************/ header.toga nav.menubar .menu { - color: var(--sl-color-neutral-200); + color: var(--wa-color-brand-on-loud); padding: 0.5em; } header.toga nav.menubar .menu:hover { - background-color: var(--sl-color-primary-700); - color: var(--sl-color-primary-300); + background-color: var(--wa-color-brand-40); + color: var(--wa-color-brand-on-loud); } header.toga nav.menubar .app { - color: var(--sl-color-neutral-50); - font-weight: var(--sl-font-weight-bold); + color: var(--wa-color-brand-on-loud); + font-weight: var(--wa-font-weight-bold); } /********************************************************************** @@ -106,3 +106,17 @@ main.toga.window > .container { .toga .label { white-space: nowrap; } + +/***** ActivityIndicator *********************************************/ +wa-spinner.stopped { + visibility: hidden; +} + +/***** Slider *********************************************************/ +/* wa-slider defaults to display:inline (custom element default), which + ignores width and doesn't stretch in flex containers. sl-range set + display:block internally; wa-slider does not. */ +.toga wa-slider { + display: block; + width: 100%; +} diff --git a/web/src/toga_web/widgets/activityindicator.py b/web/src/toga_web/widgets/activityindicator.py index 9366342146..488c3f580e 100644 --- a/web/src/toga_web/widgets/activityindicator.py +++ b/web/src/toga_web/widgets/activityindicator.py @@ -3,16 +3,16 @@ class ActivityIndicator(Widget): def create(self): - self.native = self._create_native_widget("sl-spinner") + self.native = self._create_native_widget("wa-spinner") self.stop() # Actions def start(self): - self.native.style.visibility = "visible" + self.native.classList.remove("stopped") self._is_running = True def stop(self): - self.native.style.visibility = "hidden" + self.native.classList.add("stopped") self._is_running = False def is_running(self): diff --git a/web/src/toga_web/widgets/base.py b/web/src/toga_web/widgets/base.py index 8c1b73cae6..f0d27cdcad 100644 --- a/web/src/toga_web/widgets/base.py +++ b/web/src/toga_web/widgets/base.py @@ -1,6 +1,90 @@ from abc import ABC, abstractmethod -from toga_web.libs import create_element +import js +from pyodide.ffi import JsProxy + +from toga_web.libs import create_element, create_proxy + + +class NativeProxy: + """Wraps a WebAwesome custom element, buffering attribute sets that happen + before the element's tag has been defined and the instance upgraded. + + Toga's widget lifecycle creates elements detached from the DOM and + immediately sets class-defined properties on them (e.g. `wa-switch.checked`). + Those properties only exist after the browser has (a) loaded the component + module and called `customElements.define` and (b) upgraded the specific + element instance. Until then, attribute sets would fail. + + The proxy: + * buffers pre-upgrade sets in an insertion-ordered dict + * returns the buffered value (or passes through) on reads + * once `whenDefined` resolves for the element's tag, force-upgrades the + (possibly still-disconnected) element with `customElements.upgrade` + and replays the buffer onto the now-upgraded element + """ + + def __init__(self, element): + # when mutating these bookkeeping members, + # call object.__setattr__ to avoid calling our own __setattr__ override + object.__setattr__(self, "_element", element) + object.__setattr__(self, "_pending", {}) + object.__setattr__(self, "_upgraded", False) + + tag = element.tagName.lower() + if tag.startswith("wa-"): + + def on_defined(promise): + js.customElements.upgrade(element) + object.__setattr__(self, "_upgraded", True) + for attr, value in self._pending.items(): + setattr(element, attr, value) + self._pending.clear() + + js.customElements.whenDefined(tag).then(create_proxy(on_defined)) + else: + object.__setattr__(self, "_upgraded", True) + + def unwrap(self): + """Return the underlying JsProxy element. + + Pyodide unwraps JsProxy arguments automatically when marshaling into + JS, but it does not unwrap arbitrary Python objects — so callers + that pass a NativeProxy as an *argument* to a JS method (e.g. + appendChild, insertBefore) must call unwrap() to hand JS the real Node. + """ + return self._element + + def __getattr__(self, name): + # If we're still waiting to be upgraded and we've seen a set, return that value + if not self._upgraded and name in self._pending: + return self._pending[name] + + attr = getattr(self._element, name) + # if we're asking for a JsProxy callable, assume we're going to call it; + # return wrapper that checks all args and unwraps any NativeProxys + # to their underlying JsProxy elements + if isinstance(attr, JsProxy) and callable(attr): + + def _auto_unwrap(*args): + unwrapped_args = [ + a.unwrap() if isinstance(a, NativeProxy) else a for a in args + ] + return attr(*unwrapped_args) + + return _auto_unwrap + + # otherwise just return what was asked for + return attr + + def __setattr__(self, name, value): + if self._upgraded: + setattr(self._element, name, value) + else: + # Pop-then-reinsert so replay order mirrors write order even + # when the same key is set more than once pre-upgrade. + self._pending.pop(name, None) + self._pending[name] = value class Widget(ABC): @@ -46,7 +130,7 @@ def _create_native_widget( **properties, ) - return native + return NativeProxy(native) @abstractmethod def create(self): ... diff --git a/web/src/toga_web/widgets/box.py b/web/src/toga_web/widgets/box.py index 04927a6688..030d3e4fdc 100644 --- a/web/src/toga_web/widgets/box.py +++ b/web/src/toga_web/widgets/box.py @@ -7,3 +7,14 @@ def create(self): def add_child(self, child): self.native.appendChild(child.native) + + def insert_child(self, index, child): + children = self.native.children + if index < children.length: + self.native.insertBefore(child.native, children.item(index)) + else: + self.native.appendChild(child.native) + + def remove_child(self, child): + self.native.removeChild(child.native) + child.container = None diff --git a/web/src/toga_web/widgets/button.py b/web/src/toga_web/widgets/button.py index 07e003ccc8..eebbd1f956 100644 --- a/web/src/toga_web/widgets/button.py +++ b/web/src/toga_web/widgets/button.py @@ -5,7 +5,8 @@ class Button(Widget): def create(self): - self.native = self._create_native_widget("sl-button") + self.native = self._create_native_widget("wa-button") + self.native.setAttribute("appearance", "outlined") self.native.addEventListener("click", create_proxy(self.dom_click)) def dom_click(self, event): @@ -31,5 +32,14 @@ def set_enabled(self, value): def set_background_color(self, value): pass + def _reapply_style(self): + # wa-button (appearance="outlined") sets text color via --wa-color-on-quiet + # inside its shadow DOM, so the host's inherited `color` doesn't reach the + # button label. Forward it as a CSS variable so the shadow DOM picks it up. + css = self.interface.style.__css__() + if color := self.interface.style.color: + css += f" --wa-color-on-quiet: {color};" + self.native.style = css + def rehint(self): pass diff --git a/web/src/toga_web/widgets/dateinput.py b/web/src/toga_web/widgets/dateinput.py index 990c0f95ff..47c75ec90c 100644 --- a/web/src/toga_web/widgets/dateinput.py +++ b/web/src/toga_web/widgets/dateinput.py @@ -1,5 +1,6 @@ import datetime +from toga.widgets.dateinput import MAX_DATE, MIN_DATE from toga_web.libs import create_proxy from .base import Widget @@ -15,12 +16,12 @@ def native_date(py_date_obj): class DateInput(Widget): def create(self): - self.native = self._create_native_widget("sl-input") + self.native = self._create_native_widget("wa-input") self.native.type = "date" self.native.value = native_date(datetime.date.today()) - self.native.addEventListener("sl-change", create_proxy(self.dom_sl_change)) + self.native.addEventListener("change", create_proxy(self.dom_change)) - def dom_sl_change(self, event): + def dom_change(self, event): try: input_date = py_date(self.native.value) except Exception: @@ -53,14 +54,22 @@ def set_min_date(self, value): self.native.min = native_date(value) def get_min_date(self): - if self.native.min is None: - return datetime.date(1800, 1, 1) - return datetime.date.fromisoformat(str(self.native.min)) + try: + native_min = self.native.min + except AttributeError: + return MIN_DATE + if native_min is None: + return MIN_DATE + return datetime.date.fromisoformat(str(native_min)) def set_max_date(self, value): self.native.max = native_date(value) def get_max_date(self): - if self.native.max is None: - return datetime.date(8999, 12, 31) - return datetime.date.fromisoformat(str(self.native.max)) + try: + native_max = self.native.max + except AttributeError: + return MAX_DATE + if native_max is None: + return MAX_DATE + return datetime.date.fromisoformat(str(native_max)) diff --git a/web/src/toga_web/widgets/divider.py b/web/src/toga_web/widgets/divider.py index 9f01865058..25ab0dd18a 100644 --- a/web/src/toga_web/widgets/divider.py +++ b/web/src/toga_web/widgets/divider.py @@ -5,13 +5,13 @@ class Divider(Widget): def create(self): - self.native = self._create_native_widget("sl-divider") + self.native = self._create_native_widget("wa-divider") def get_direction(self): return self.interface.direction def set_direction(self, value): if value is Direction.VERTICAL: - self.native.setAttribute("vertical", "") + self.native.setAttribute("orientation", "vertical") else: - self.native.removeAttribute("vertical") + self.native.removeAttribute("orientation") diff --git a/web/src/toga_web/widgets/progressbar.py b/web/src/toga_web/widgets/progressbar.py index de65bf9be3..d690c84619 100644 --- a/web/src/toga_web/widgets/progressbar.py +++ b/web/src/toga_web/widgets/progressbar.py @@ -3,7 +3,7 @@ class ProgressBar(Widget): def create(self): - self.native = self._create_native_widget("sl-progress-bar") + self.native = self._create_native_widget("wa-progress-bar") self._is_running = False self._value = 0 self._max = 0 diff --git a/web/src/toga_web/widgets/selection.py b/web/src/toga_web/widgets/selection.py index ed62a21d9c..d61d308469 100644 --- a/web/src/toga_web/widgets/selection.py +++ b/web/src/toga_web/widgets/selection.py @@ -7,10 +7,10 @@ class Selection(Widget): def create(self): - self.native = self._create_native_widget("sl-select") - self.native.addEventListener("sl-change", create_proxy(self.dom_sl_change)) + self.native = self._create_native_widget("wa-select") + self.native.addEventListener("change", create_proxy(self.dom_change)) - def dom_sl_change(self, event): + def dom_change(self, event): self.interface.on_change() # Alias for backwards compatibility: @@ -45,10 +45,14 @@ def insert(self, index, item): def source_insert(self, *, index, item): display_text = self.interface._title_for_item(item) - option = self._create_native_widget("sl-option") + option = self._create_native_widget("wa-option") option.value = str(index) option.textContent = display_text - if self.native.value == "": + try: + native_value = self.native.value + except AttributeError: + native_value = "" + if native_value == "": self.native.value = option.value if index >= len(self.native.children): self.native.appendChild(option) @@ -56,13 +60,17 @@ def source_insert(self, *, index, item): self.native.insertBefore(option, self.native.children[index]) def get_selected_index(self): - if self.native.value: - return int(self.native.value) + try: + value = self.native.value + except AttributeError: + value = "" + if value: + return int(value) return None def select_item(self, index, item): self.native.value = str(index) - self.native.dispatchEvent(CustomEvent.new("sl-change")) + self.native.dispatchEvent(CustomEvent.new("change")) def rehint(self): pass diff --git a/web/src/toga_web/widgets/slider.py b/web/src/toga_web/widgets/slider.py index 7249261154..08a805bbcd 100644 --- a/web/src/toga_web/widgets/slider.py +++ b/web/src/toga_web/widgets/slider.py @@ -1,3 +1,5 @@ +import js + from toga_web.libs import create_proxy from .base import Widget @@ -5,45 +7,67 @@ class Slider(Widget): def create(self): - self.native = self._create_native_widget("sl-range") - self.native.addEventListener("sl-input", create_proxy(self.dom_sl_input)) + self._dragging = False + self.native = self._create_native_widget("wa-slider") + self.native.addEventListener("input", create_proxy(self.dom_input)) self.native.addEventListener( "pointerdown", create_proxy(self.dom_onpointerdown) ) - self.native.addEventListener("pointerup", create_proxy(self.dom_onpointerup)) + # wa-slider has no pointer capture (unlike sl-range, which wrapped a native + # that the browser captures implicitly). Without capture, + # releasing outside the element fires pointerup on whatever is under the + # pointer,not on the slider — so on_release would miss and could fire on a + # different widget. Listening on document with a _dragging guard fixes both + # problems. + js.document.addEventListener("pointerup", create_proxy(self.dom_onpointerup)) - def dom_sl_input(self, event): + def dom_input(self, event): self.interface.value = float(self.native.value) if self.interface.on_change: self.interface.on_change() def dom_onpointerdown(self, event): + self._dragging = True self.interface.on_press() def dom_onpointerup(self, event): - self.interface.on_release() + if self._dragging: + self._dragging = False + self.interface.on_release() def get_value(self): - return float(self.native.value) + try: + return float(self.native.value) + except AttributeError: + return 0.0 def set_value(self, value): self.native.value = value def get_min(self): - return float(self.native.min) + try: + return float(self.native.min) + except AttributeError: + return 0.0 def set_min(self, value): self.native.min = value def get_max(self): - return float(self.native.max) + try: + return float(self.native.max) + except AttributeError: + return 1.0 def set_max(self, value): self.native.max = value def get_tick_count(self): - step = float(self.native.step or 0) - return int((float(self.native.max) - float(self.native.min)) / step) + 1 + try: + step = float(self.native.step or 0) + return int((float(self.native.max) - float(self.native.min)) / step) + 1 + except AttributeError: + return None def set_tick_count(self, tick_count): if tick_count: diff --git a/web/src/toga_web/widgets/switch.py b/web/src/toga_web/widgets/switch.py index 558f837661..678744bb1d 100644 --- a/web/src/toga_web/widgets/switch.py +++ b/web/src/toga_web/widgets/switch.py @@ -5,8 +5,8 @@ class Switch(Widget): def create(self): - self.native = self._create_native_widget("sl-switch") - self.native.addEventListener("sl-change", create_proxy(self.dom_onchange)) + self.native = self._create_native_widget("wa-switch") + self.native.addEventListener("change", create_proxy(self.dom_onchange)) def dom_onchange(self, event): self.interface.on_change() @@ -18,10 +18,22 @@ def set_text(self, text): self.native.innerHTML = text def get_value(self): - return self.native.checked + try: + return self.native.checked + except AttributeError: + return False def set_value(self, value): old_value = self.get_value() self.native.checked = value if value != old_value: self.interface.on_change() + + def _reapply_style(self): + # wa-switch sets color via --wa-form-control-value-color inside its shadow DOM, + # so the host's inherited `color` property doesn't reach the label text. + # Forward it explicitly as a CSS variable so the shadow DOM picks it up. + css = self.interface.style.__css__() + if color := self.interface.style.color: + css += f" --wa-form-control-value-color: {color};" + self.native.style = css diff --git a/web/src/toga_web/widgets/textinput.py b/web/src/toga_web/widgets/textinput.py index 1760983208..b8a566a740 100644 --- a/web/src/toga_web/widgets/textinput.py +++ b/web/src/toga_web/widgets/textinput.py @@ -6,15 +6,15 @@ class TextInput(Widget): def create(self): self._return_listener = None - self.native = self._create_native_widget("sl-input") + self.native = self._create_native_widget("wa-input") 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)) + self.native.addEventListener("change", create_proxy(self.dom_change)) def dom_onkeyup(self, event): self.interface.on_change() - def dom_sl_change(self, event): + def dom_change(self, event): self.interface.on_confirm() def set_readonly(self, value): diff --git a/web/src/toga_web/widgets/timeinput.py b/web/src/toga_web/widgets/timeinput.py index 807876f5ed..7ba8e708c3 100644 --- a/web/src/toga_web/widgets/timeinput.py +++ b/web/src/toga_web/widgets/timeinput.py @@ -27,13 +27,13 @@ def native_time(py_time): class TimeInput(Widget): def create(self): - self.native = self._create_native_widget("sl-input") + self.native = self._create_native_widget("wa-input") self.native.setAttribute("step", "1") # force seconds to show self.native.type = "time" self.native.value = native_time(datetime.datetime.now()) - self.native.addEventListener("sl-change", create_proxy(self.dom_sl_change)) + self.native.addEventListener("change", create_proxy(self.dom_change)) - def dom_sl_change(self, event): + def dom_change(self, event): try: input_time = py_time(self.native.value) except Exception: @@ -62,14 +62,22 @@ def set_value(self, value): self.native.value = native_time(value) def get_min_time(self): - min_value = self.native.min if self.native.min else datetime.time(0, 0, 0) + try: + min_value = self.native.min if self.native.min else datetime.time(0, 0, 0) + except AttributeError: + min_value = datetime.time(0, 0, 0) return py_time(min_value) def set_min_time(self, value): self.native.min = native_time(value) def get_max_time(self): - max_value = self.native.max if self.native.max else datetime.time(23, 59, 59) + try: + max_value = ( + self.native.max if self.native.max else datetime.time(23, 59, 59) + ) + except AttributeError: + max_value = datetime.time(23, 59, 59) return py_time(max_value) def set_max_time(self, value): diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index 9dbfdc9771..0a67de71eb 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -96,8 +96,12 @@ def set_content(self, widget): for child in self.native.childNodes: self.native.removeChild(child) - # Add all children to the content widget. - self.native.appendChild(widget.native) + # widget.native is a NativeProxy (a Python wrapper, not a JsProxy). + # NativeProxy.__getattr__ auto-unwraps NativeProxy arguments when you + # call a JS method on a NativeProxy — but self.native here is a plain + # JsProxy (Window doesn't use _create_native_widget), so auto-unwrap + # doesn't apply. We must manually unwrap to give JS a real Node. + self.native.appendChild(widget.native.unwrap()) ###################################################################### # Window size @@ -156,7 +160,7 @@ def get_image_data(self): class MainWindow(Window): def _create_submenu(self, group, items): submenu = create_element( - "sl-dropdown", + "wa-dropdown", children=[ create_element( "span", @@ -165,11 +169,8 @@ def _create_submenu(self, group, items): slot="trigger", content=group.text, ), - create_element( - "sl-menu", - children=items, - ), - ], + ] + + items, ) return submenu @@ -187,7 +188,7 @@ def create_menus(self): submenu = self._menu_groups.setdefault(cmd.group, []) menu_item = create_element( - "sl-menu-item", + "wa-dropdown-item", content=cmd.text, disabled=not cmd.enabled, )