From 0def72bc6170c6b5ddeef4889a6d201a9c2144cd Mon Sep 17 00:00:00 2001 From: Ahter Sonmez Date: Sun, 23 Jul 2023 13:17:17 +0100 Subject: [PATCH 1/5] Add a web widget for the activity indicator This is the web implementation of the spinner component. The details can be found in: https://shoelace.style/components/spinner --- changes/2050.feature.rst | 1 + web/src/toga_web/widgets/activityindicator.py | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 changes/2050.feature.rst create mode 100644 web/src/toga_web/widgets/activityindicator.py diff --git a/changes/2050.feature.rst b/changes/2050.feature.rst new file mode 100644 index 0000000000..3cf6fcd4e6 --- /dev/null +++ b/changes/2050.feature.rst @@ -0,0 +1 @@ +Activity indicator now has a web widget. diff --git a/web/src/toga_web/widgets/activityindicator.py b/web/src/toga_web/widgets/activityindicator.py new file mode 100644 index 0000000000..a2b50c2f91 --- /dev/null +++ b/web/src/toga_web/widgets/activityindicator.py @@ -0,0 +1,28 @@ +""" +A web widget to display an activity indicator. + +This is a web implementation of the `toga.ActivityIndicator` widget. + +Details can be found in +https://shoelace.style/components/spinner +""" +from .base import Widget + + +class ActivityIndicator(Widget): + def create(self): + self.interface.style._use_default_width = False + self.native = self._create_native_widget("sl-spinner") + self.stop() + + # Actions + def start(self): + self.native.style.visibility = "visible" + self._is_running = True + + def stop(self): + self.native.style.visibility = "hidden" + self._is_running = False + + def is_running(self): + return self._is_running From 4b05cfedd5c2979d7ee02c68d671bc351c297003 Mon Sep 17 00:00:00 2001 From: Ahter Sonmez Date: Sun, 23 Jul 2023 13:44:07 +0100 Subject: [PATCH 2/5] Add ActivityIndicator for the web factories Prior to this change, ActivityIndicator wasn't part of the factory for the web example apps. This change imports and adds it so that it can be tested out. --- web/src/toga_web/factory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/toga_web/factory.py b/web/src/toga_web/factory.py index be253f674f..8ca7af7735 100644 --- a/web/src/toga_web/factory.py +++ b/web/src/toga_web/factory.py @@ -8,6 +8,7 @@ # from .images import Image from .paths import Paths +from .widgets.activityindicator import ActivityIndicator from .widgets.box import Box from .widgets.button import Button @@ -64,6 +65,7 @@ def not_implemented(feature): # 'OptionContainer', # 'PasswordInput', "ProgressBar", + "ActivityIndicator", # 'ScrollContainer', # 'Selection', # 'Slider', From 8a8229791f0175921d1c7051cf21bf29c1aa7813 Mon Sep 17 00:00:00 2001 From: Ahter Sonmez Date: Sun, 23 Jul 2023 15:54:47 +0100 Subject: [PATCH 3/5] Set flex only when using the default width This was necessary to override the behavior of adding flex element to the style. The spinner (activity indicator) needs to have this set to false. --- core/src/toga/style/pack.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index 6208d1f604..7e9650eb25 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -764,9 +764,10 @@ def __css__(self): # direction css.append(f"flex-direction: {self.direction.lower()};") # flex - if (self.width == NONE and self.direction == ROW) or ( - self.height == NONE and self.direction == COLUMN - ): + if ( + (self.width == NONE and self.direction == ROW) + or (self.height == NONE and self.direction == COLUMN) + ) and getattr(self, "_use_default_width", True): css.append(f"flex: {self.flex} 0 0;") # width/flex From db1bdfe4e86ae0e909e8137d3c8855aebb5c1657 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 26 Jul 2023 11:47:21 +0800 Subject: [PATCH 4/5] An alternate solution to the box sizing problem. --- core/src/toga/style/pack.py | 9 +- core/tests/style/pack/test_css.py | 146 +++++++++--------- docs/reference/style/pack.rst | 2 +- web/src/toga_web/widgets/activityindicator.py | 9 -- 4 files changed, 78 insertions(+), 88 deletions(-) diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index 7e9650eb25..0fd90facb2 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -764,11 +764,10 @@ def __css__(self): # direction css.append(f"flex-direction: {self.direction.lower()};") # flex - if ( - (self.width == NONE and self.direction == ROW) - or (self.height == NONE and self.direction == COLUMN) - ) and getattr(self, "_use_default_width", True): - css.append(f"flex: {self.flex} 0 0;") + if (self.width == NONE and self.direction == ROW) or ( + self.height == NONE and self.direction == COLUMN + ): + css.append(f"flex: {self.flex} 0 auto;") # width/flex if self.width != NONE: diff --git a/core/tests/style/pack/test_css.py b/core/tests/style/pack/test_css.py index 6562754230..f204c79d1e 100644 --- a/core/tests/style/pack/test_css.py +++ b/core/tests/style/pack/test_css.py @@ -32,40 +32,40 @@ # Empty definition pytest.param( Pack(), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="empty", ), # Display pytest.param( Pack(display=PACK), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="display-pack", ), pytest.param( Pack(display=NONE), - "display: none; flex-direction: row; flex: 0.0 0 0;", + "display: none; flex-direction: row; flex: 0.0 0 auto;", id="display-none", ), # Visibility pytest.param( Pack(visibility=VISIBLE), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="visibility-visible", ), pytest.param( Pack(visibility=HIDDEN), - "visibility: hidden; flex-direction: row; flex: 0.0 0 0;", + "visibility: hidden; flex-direction: row; flex: 0.0 0 auto;", id="visibility-hidden", ), # Direction pytest.param( Pack(direction=ROW), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="direction-row", ), pytest.param( Pack(direction=COLUMN), - "flex-direction: column; flex: 0.0 0 0;", + "flex-direction: column; flex: 0.0 0 auto;", id="direction-column", ), # Width @@ -86,12 +86,12 @@ ), pytest.param( Pack(width=NONE), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="width-none", ), pytest.param( Pack(width=NONE, flex=5), - "flex-direction: row; flex: 5.0 0 0;", + "flex-direction: row; flex: 5.0 0 auto;", id="width-none-flex", ), pytest.param( @@ -111,88 +111,88 @@ ), pytest.param( Pack(direction=ROW, width=NONE), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="width-row-none", ), pytest.param( Pack(direction=ROW, width=NONE, flex=5), - "flex-direction: row; flex: 5.0 0 0;", + "flex-direction: row; flex: 5.0 0 auto;", id="width-row-none-flex", ), pytest.param( Pack(direction=COLUMN, width=42), - "flex-direction: column; flex: 0.0 0 0; width: 42px;", + "flex-direction: column; flex: 0.0 0 auto; width: 42px;", id="width-column-explicit", ), pytest.param( Pack(direction=COLUMN, width=42, flex=5), - "flex-direction: column; flex: 5.0 0 0; width: 42px;", + "flex-direction: column; flex: 5.0 0 auto; width: 42px;", id="width-column-explicit-flex", ), pytest.param( Pack(direction=COLUMN, width=0), - "flex-direction: column; flex: 0.0 0 0; width: 0px;", + "flex-direction: column; flex: 0.0 0 auto; width: 0px;", id="width-column-0", ), pytest.param( Pack(direction=COLUMN, width=NONE), - "flex-direction: column; flex: 0.0 0 0;", + "flex-direction: column; flex: 0.0 0 auto;", id="width-column-none", ), pytest.param( Pack(direction=COLUMN, width=NONE, flex=5), - "flex-direction: column; flex: 5.0 0 0;", + "flex-direction: column; flex: 5.0 0 auto;", id="width-column-none-flex", ), # Height pytest.param( Pack(height=42), - "flex-direction: row; flex: 0.0 0 0; height: 42px;", + "flex-direction: row; flex: 0.0 0 auto; height: 42px;", id="height-explicit", ), pytest.param( Pack(height=42, flex=5), - "flex-direction: row; flex: 5.0 0 0; height: 42px;", + "flex-direction: row; flex: 5.0 0 auto; height: 42px;", id="height-explicit-flex", ), pytest.param( Pack(height=0), - "flex-direction: row; flex: 0.0 0 0; height: 0px;", + "flex-direction: row; flex: 0.0 0 auto; height: 0px;", id="height-0", ), pytest.param( Pack(height=NONE), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="height-none", ), pytest.param( Pack(height=NONE, flex=5), - "flex-direction: row; flex: 5.0 0 0;", + "flex-direction: row; flex: 5.0 0 auto;", id="height-none-flex", ), pytest.param( Pack(direction=ROW, height=42), - "flex-direction: row; flex: 0.0 0 0; height: 42px;", + "flex-direction: row; flex: 0.0 0 auto; height: 42px;", id="height-row-explicit", ), pytest.param( Pack(direction=ROW, height=42, flex=5), - "flex-direction: row; flex: 5.0 0 0; height: 42px;", + "flex-direction: row; flex: 5.0 0 auto; height: 42px;", id="height-row-explicit", ), pytest.param( Pack(direction=ROW, height=0), - "flex-direction: row; flex: 0.0 0 0; height: 0px;", + "flex-direction: row; flex: 0.0 0 auto; height: 0px;", id="height-row-0", ), pytest.param( Pack(direction=ROW, height=NONE), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="height-row-none", ), pytest.param( Pack(direction=ROW, height=NONE, flex=5), - "flex-direction: row; flex: 5.0 0 0;", + "flex-direction: row; flex: 5.0 0 auto;", id="height-row-none", ), pytest.param( @@ -212,117 +212,117 @@ ), pytest.param( Pack(direction=COLUMN, height=NONE), - "flex-direction: column; flex: 0.0 0 0;", + "flex-direction: column; flex: 0.0 0 auto;", id="height-column-none", ), pytest.param( Pack(direction=COLUMN, height=NONE, flex=5), - "flex-direction: column; flex: 5.0 0 0;", + "flex-direction: column; flex: 5.0 0 auto;", id="height-column-none", ), # Alignment - default layout pytest.param( Pack(alignment=LEFT), - "flex-direction: row; flex: 0.0 0 0; align-items: start;", + "flex-direction: row; flex: 0.0 0 auto; align-items: start;", id="alignment-left", ), pytest.param( Pack(alignment=RIGHT), - "flex-direction: row; flex: 0.0 0 0; align-items: end;", + "flex-direction: row; flex: 0.0 0 auto; align-items: end;", id="alignment-right", ), pytest.param( # alignment is ignored Pack(alignment=TOP), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="alignment-top", ), pytest.param( # alignment is ignored Pack(alignment=BOTTOM), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="alignment-bottom", ), pytest.param( Pack(alignment=CENTER), - "flex-direction: row; flex: 0.0 0 0; align-items: center;", + "flex-direction: row; flex: 0.0 0 auto; align-items: center;", id="alignment-center", ), # Alignment - row layout pytest.param( Pack(direction=ROW, alignment=LEFT), - "flex-direction: row; flex: 0.0 0 0; align-items: start;", + "flex-direction: row; flex: 0.0 0 auto; align-items: start;", id="row-alignment-left", ), pytest.param( Pack(direction=ROW, alignment=RIGHT), - "flex-direction: row; flex: 0.0 0 0; align-items: end;", + "flex-direction: row; flex: 0.0 0 auto; align-items: end;", id="row-alignment-right", ), pytest.param( # alignment is ignored Pack(direction=ROW, alignment=TOP), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="row-alignment-top", ), pytest.param( # alignment is ignored Pack(direction=ROW, alignment=BOTTOM), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="row-alignment-bottom", ), pytest.param( Pack(direction=ROW, alignment=CENTER), - "flex-direction: row; flex: 0.0 0 0; align-items: center;", + "flex-direction: row; flex: 0.0 0 auto; align-items: center;", id="row-alignment-center", ), # Alignment - column layout pytest.param( # alignment is ignored Pack(direction=COLUMN, alignment=LEFT), - "flex-direction: column; flex: 0.0 0 0;", + "flex-direction: column; flex: 0.0 0 auto;", id="column-alignment-left", ), pytest.param( # alignment is ignored Pack(direction=COLUMN, alignment=RIGHT), - "flex-direction: column; flex: 0.0 0 0;", + "flex-direction: column; flex: 0.0 0 auto;", id="column-alignment-right", ), pytest.param( Pack(direction=COLUMN, alignment=TOP), - "flex-direction: column; flex: 0.0 0 0; align-items: start;", + "flex-direction: column; flex: 0.0 0 auto; align-items: start;", id="column-alignment-top", ), pytest.param( Pack(direction=COLUMN, alignment=BOTTOM), - "flex-direction: column; flex: 0.0 0 0; align-items: end;", + "flex-direction: column; flex: 0.0 0 auto; align-items: end;", id="column-alignment-bottom", ), pytest.param( Pack(direction=COLUMN, alignment=CENTER), - "flex-direction: column; flex: 0.0 0 0; align-items: center;", + "flex-direction: column; flex: 0.0 0 auto; align-items: center;", id="column-alignment-center", ), # Padding pytest.param( Pack(padding_top=42), - "flex-direction: row; flex: 0.0 0 0; margin-top: 42px;", + "flex-direction: row; flex: 0.0 0 auto; margin-top: 42px;", id="padding-top", ), pytest.param( Pack(padding_bottom=42), - "flex-direction: row; flex: 0.0 0 0; margin-bottom: 42px;", + "flex-direction: row; flex: 0.0 0 auto; margin-bottom: 42px;", id="padding-bottom", ), pytest.param( Pack(padding_left=42), - "flex-direction: row; flex: 0.0 0 0; margin-left: 42px;", + "flex-direction: row; flex: 0.0 0 auto; margin-left: 42px;", id="padding-left", ), pytest.param( Pack(padding_right=42), - "flex-direction: row; flex: 0.0 0 0; margin-right: 42px;", + "flex-direction: row; flex: 0.0 0 auto; margin-right: 42px;", id="padding-right", ), pytest.param( Pack(padding=42), ( - "flex-direction: row; flex: 0.0 0 0; " + "flex-direction: row; flex: 0.0 0 auto; " "margin-top: 42px; margin-bottom: 42px; " "margin-left: 42px; margin-right: 42px;" ), @@ -331,127 +331,127 @@ # Explicitly 0 padding pytest.param( Pack(padding_top=0), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="padding-top-0", ), pytest.param( Pack(padding_bottom=0), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="padding-bottom-0", ), pytest.param( Pack(padding_left=0), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="padding-left-0", ), pytest.param( Pack(padding_right=0), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="padding-right-0", ), pytest.param( Pack(padding=0), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="padding-0", ), # Color pytest.param( Pack(color=REBECCAPURPLE), - "flex-direction: row; flex: 0.0 0 0; color: rgb(102, 51, 153);", + "flex-direction: row; flex: 0.0 0 auto; color: rgb(102, 51, 153);", id="color", ), # Background Color pytest.param( Pack(background_color=REBECCAPURPLE), - "flex-direction: row; flex: 0.0 0 0; background-color: rgb(102, 51, 153);", + "flex-direction: row; flex: 0.0 0 auto; background-color: rgb(102, 51, 153);", id="background-color", ), # Text Alignment pytest.param( Pack(text_align=LEFT), - "flex-direction: row; flex: 0.0 0 0; text-align: left;", + "flex-direction: row; flex: 0.0 0 auto; text-align: left;", id="text-align-left", ), pytest.param( Pack(text_align=RIGHT), - "flex-direction: row; flex: 0.0 0 0; text-align: right;", + "flex-direction: row; flex: 0.0 0 auto; text-align: right;", id="text-align-right", ), pytest.param( Pack(text_align=CENTER), - "flex-direction: row; flex: 0.0 0 0; text-align: center;", + "flex-direction: row; flex: 0.0 0 auto; text-align: center;", id="text-align-center", ), pytest.param( Pack(text_align=JUSTIFY), - "flex-direction: row; flex: 0.0 0 0; text-align: justify;", + "flex-direction: row; flex: 0.0 0 auto; text-align: justify;", id="text-align-justify", ), # Text Direction pytest.param( Pack(text_direction=RTL), - "flex-direction: row; flex: 0.0 0 0; text-direction: rtl;", + "flex-direction: row; flex: 0.0 0 auto; text-direction: rtl;", id="text-align-rtl", ), pytest.param( Pack(text_direction=LTR), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="text-align-ltr", ), # Font pytest.param( Pack(font_family="Helvetica"), - "flex-direction: row; flex: 0.0 0 0; font-family: Helvetica;", + "flex-direction: row; flex: 0.0 0 auto; font-family: Helvetica;", id="font-family", ), pytest.param( Pack(font_family="Times New Roman"), - 'flex-direction: row; flex: 0.0 0 0; font-family: "Times New Roman";', + 'flex-direction: row; flex: 0.0 0 auto; font-family: "Times New Roman";', id="font-family", ), pytest.param( Pack(font_family=SYSTEM), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="font-family-explicit-default", ), pytest.param( Pack(font_style=ITALIC), - "flex-direction: row; flex: 0.0 0 0; font-style: italic;", + "flex-direction: row; flex: 0.0 0 auto; font-style: italic;", id="font-style", ), pytest.param( Pack(font_style=NORMAL), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="font-style-explicit-default", ), pytest.param( Pack(font_weight=BOLD), - "flex-direction: row; flex: 0.0 0 0; font-weight: bold;", + "flex-direction: row; flex: 0.0 0 auto; font-weight: bold;", id="font-weight", ), pytest.param( Pack(font_weight=NORMAL), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="font-weight-explicit-default", ), pytest.param( Pack(font_variant=SMALL_CAPS), - "flex-direction: row; flex: 0.0 0 0; font-variant: small-caps;", + "flex-direction: row; flex: 0.0 0 auto; font-variant: small-caps;", id="font-variant", ), pytest.param( Pack(font_variant=NORMAL), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="font-variant-explicit-default", ), pytest.param( Pack(font_size=42), - "flex-direction: row; flex: 0.0 0 0; font-size: 42pt;", + "flex-direction: row; flex: 0.0 0 auto; font-size: 42pt;", id="font-size", ), pytest.param( Pack(font_size=SYSTEM_DEFAULT_FONT_SIZE), - "flex-direction: row; flex: 0.0 0 0;", + "flex-direction: row; flex: 0.0 0 auto;", id="font-size-explicit-default", ), pytest.param( @@ -463,7 +463,7 @@ font_size=42, ), ( - "flex-direction: row; flex: 0.0 0 0; " + "flex-direction: row; flex: 0.0 0 auto; " "font-family: Helvetica; " "font-size: 42pt; " "font-weight: bold; " diff --git a/docs/reference/style/pack.rst b/docs/reference/style/pack.rst index 57209ee5d3..3dd07c0c56 100644 --- a/docs/reference/style/pack.rst +++ b/docs/reference/style/pack.rst @@ -314,7 +314,7 @@ The mapping that can be used to establish the reference implementation is: ``display: pack`` ``display: flex`` ``flex: `` If ``direction = row`` and ``width`` is set, or ``direction = column`` and ``height`` is set, - ignore. Otherwise, ``flex: 0 0``. + ignore. Otherwise, ``flex: 0 auto``. ``font_size: `` ``font-size: pt`` ``height: `` ``height: px`` if value is an integer; ``height: auto`` if value is ``none``. diff --git a/web/src/toga_web/widgets/activityindicator.py b/web/src/toga_web/widgets/activityindicator.py index a2b50c2f91..9366342146 100644 --- a/web/src/toga_web/widgets/activityindicator.py +++ b/web/src/toga_web/widgets/activityindicator.py @@ -1,17 +1,8 @@ -""" -A web widget to display an activity indicator. - -This is a web implementation of the `toga.ActivityIndicator` widget. - -Details can be found in -https://shoelace.style/components/spinner -""" from .base import Widget class ActivityIndicator(Widget): def create(self): - self.interface.style._use_default_width = False self.native = self._create_native_widget("sl-spinner") self.stop() From 64dc969e79ac2e823b7153d761df989c2bb292b6 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 26 Jul 2023 11:47:39 +0800 Subject: [PATCH 5/5] Update changenote and widget support matrix. --- changes/2050.feature.rst | 2 +- docs/reference/data/widgets_by_platform.csv | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changes/2050.feature.rst b/changes/2050.feature.rst index 3cf6fcd4e6..e1632062c5 100644 --- a/changes/2050.feature.rst +++ b/changes/2050.feature.rst @@ -1 +1 @@ -Activity indicator now has a web widget. +An implementation of ActivityIndicator was added to the Web backend. diff --git a/docs/reference/data/widgets_by_platform.csv b/docs/reference/data/widgets_by_platform.csv index c0e8abb91e..933348c101 100644 --- a/docs/reference/data/widgets_by_platform.csv +++ b/docs/reference/data/widgets_by_platform.csv @@ -2,7 +2,7 @@ Component,Type,Component,Description,macOS,GTK,Windows,iOS,Android,Web Application,Core Component,:class:`~toga.App`,The application itself,|b|,|b|,|b|,|b|,|b|,|b| Window,Core Component,:class:`~toga.Window`,Window object,|b|,|b|,|b|,|b|,|b|,|b| MainWindow,Core Component,:class:`~toga.MainWindow`,Main window of the application,|b|,|b|,|b|,|b|,|b|,|b| -ActivityIndicator,General Widget,:class:`~toga.ActivityIndicator`,A spinning activity animation,|y|,|y|,,,, +ActivityIndicator,General Widget,:class:`~toga.ActivityIndicator`,A spinning activity animation,|y|,|y|,,,,|b| Button,General Widget,:class:`~toga.Button`,Basic clickable Button,|y|,|y|,|y|,|y|,|y|,|b| Canvas,General Widget,:class:`~toga.Canvas`,Area you can draw on,|b|,|b|,|b|,|b|,, DateInput,General Widget,:class:`~toga.DateInput`,A widget to select a calendar date,,,|y|,,|y|,