From 3f41330cedc6559ce9f1decf69ec648ffdd179b1 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 6 Aug 2023 11:02:26 +0800 Subject: [PATCH 01/10] First pass at a Textual backend. --- changes/1867.feature.rst | 1 + docs/reference/api/app.rst | 4 +- docs/reference/api/containers/box.rst | 2 +- .../api/containers/optioncontainer.rst | 2 +- .../api/containers/scrollcontainer.rst | 2 +- .../api/containers/splitcontainer.rst | 2 +- docs/reference/api/mainwindow.rst | 2 +- docs/reference/api/resources/app_paths.rst | 2 +- docs/reference/api/resources/command.rst | 2 +- docs/reference/api/resources/fonts.rst | 2 +- docs/reference/api/resources/group.rst | 2 +- docs/reference/api/resources/icons.rst | 2 +- docs/reference/api/resources/images.rst | 2 +- .../api/widgets/activityindicator.rst | 2 +- docs/reference/api/widgets/button.rst | 2 +- docs/reference/api/widgets/canvas.rst | 2 +- docs/reference/api/widgets/dateinput.rst | 2 +- docs/reference/api/widgets/detailedlist.rst | 2 +- docs/reference/api/widgets/divider.rst | 2 +- docs/reference/api/widgets/imageview.rst | 2 +- docs/reference/api/widgets/label.rst | 2 +- .../api/widgets/multilinetextinput.rst | 2 +- docs/reference/api/widgets/numberinput.rst | 2 +- docs/reference/api/widgets/passwordinput.rst | 2 +- docs/reference/api/widgets/progressbar.rst | 2 +- docs/reference/api/widgets/selection.rst | 2 +- docs/reference/api/widgets/slider.rst | 2 +- docs/reference/api/widgets/switch.rst | 2 +- docs/reference/api/widgets/table.rst | 2 +- docs/reference/api/widgets/textinput.rst | 2 +- docs/reference/api/widgets/timeinput.rst | 2 +- docs/reference/api/widgets/tree.rst | 2 +- docs/reference/api/widgets/webview.rst | 2 +- docs/reference/api/widgets/widget.rst | 2 +- docs/reference/api/window.rst | 2 +- docs/reference/data/widgets_by_platform.csv | 70 +++++++------- docs/reference/platforms/index.rst | 3 +- docs/reference/platforms/terminal.rst | 36 +++++++ docs/reference/widgets_by_platform.rst | 16 ++-- nursery/textual/tests/test_implementation.py | 11 --- nursery/textual/toga_textual/__init__.py | 9 -- {nursery/textual => textual}/CONTRIBUTING.md | 0 {nursery/textual => textual}/MANIFEST.in | 0 {nursery/textual => textual}/README.rst | 15 ++- {nursery/textual => textual}/pyproject.toml | 0 {nursery/textual => textual}/setup.cfg | 28 +++--- {nursery/textual => textual}/setup.py | 3 +- textual/src/toga_textual/__init__.py | 3 + textual/src/toga_textual/app.py | 71 ++++++++++++++ textual/src/toga_textual/container.py | 39 ++++++++ textual/src/toga_textual/factory.py | 89 ++++++++++++++++++ textual/src/toga_textual/icons.py | 8 ++ textual/src/toga_textual/paths.py | 94 +++++++++++++++++++ .../src/toga_textual/widgets}/__init__.py | 0 textual/src/toga_textual/widgets/base.py | 82 ++++++++++++++++ textual/src/toga_textual/widgets/box.py | 16 ++++ textual/src/toga_textual/widgets/button.py | 24 +++++ textual/src/toga_textual/widgets/label.py | 14 +++ textual/src/toga_textual/widgets/textinput.py | 43 +++++++++ textual/src/toga_textual/window.py | 74 +++++++++++++++ textual/tests_backend/__init__.py | 0 textual/tests_backend/app.py | 0 textual/tests_backend/images.py | 0 textual/tests_backend/probe.py | 0 textual/tests_backend/widgets/__init__.py | 0 textual/tests_backend/widgets/base.py | 0 textual/tests_backend/widgets/box.py | 0 textual/tests_backend/widgets/button.py | 0 68 files changed, 692 insertions(+), 127 deletions(-) create mode 100644 changes/1867.feature.rst create mode 100644 docs/reference/platforms/terminal.rst delete mode 100644 nursery/textual/tests/test_implementation.py delete mode 100644 nursery/textual/toga_textual/__init__.py rename {nursery/textual => textual}/CONTRIBUTING.md (100%) rename {nursery/textual => textual}/MANIFEST.in (100%) rename {nursery/textual => textual}/README.rst (78%) rename {nursery/textual => textual}/pyproject.toml (100%) rename {nursery/textual => textual}/setup.cfg (84%) rename {nursery/textual => textual}/setup.py (76%) create mode 100644 textual/src/toga_textual/__init__.py create mode 100644 textual/src/toga_textual/app.py create mode 100644 textual/src/toga_textual/container.py create mode 100644 textual/src/toga_textual/factory.py create mode 100644 textual/src/toga_textual/icons.py create mode 100644 textual/src/toga_textual/paths.py rename {nursery/textual/tests => textual/src/toga_textual/widgets}/__init__.py (100%) create mode 100644 textual/src/toga_textual/widgets/base.py create mode 100644 textual/src/toga_textual/widgets/box.py create mode 100644 textual/src/toga_textual/widgets/button.py create mode 100644 textual/src/toga_textual/widgets/label.py create mode 100644 textual/src/toga_textual/widgets/textinput.py create mode 100644 textual/src/toga_textual/window.py create mode 100644 textual/tests_backend/__init__.py create mode 100644 textual/tests_backend/app.py create mode 100644 textual/tests_backend/images.py create mode 100644 textual/tests_backend/probe.py create mode 100644 textual/tests_backend/widgets/__init__.py create mode 100644 textual/tests_backend/widgets/base.py create mode 100644 textual/tests_backend/widgets/box.py create mode 100644 textual/tests_backend/widgets/button.py diff --git a/changes/1867.feature.rst b/changes/1867.feature.rst new file mode 100644 index 0000000000..98c16c6c7f --- /dev/null +++ b/changes/1867.feature.rst @@ -0,0 +1 @@ +A new Textual backend was added to support terminal applications. diff --git a/docs/reference/api/app.rst b/docs/reference/api/app.rst index ab3b036f3f..aeaa57eb92 100644 --- a/docs/reference/api/app.rst +++ b/docs/reference/api/app.rst @@ -5,8 +5,8 @@ Application .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 - :exclude: {0: '(?!(App|Component))'} + :included_cols: 4,5,6,7,8,9,10 + :exclude: {0: '(?!(Application|Component))'} The app is the main entry point and container for the Toga GUI. diff --git a/docs/reference/api/containers/box.rst b/docs/reference/api/containers/box.rst index e7df9dcaf3..e2f79747cb 100644 --- a/docs/reference/api/containers/box.rst +++ b/docs/reference/api/containers/box.rst @@ -7,7 +7,7 @@ A generic container for other widgets. Used to construct layouts. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(Box|Component))'} Usage diff --git a/docs/reference/api/containers/optioncontainer.rst b/docs/reference/api/containers/optioncontainer.rst index d31cad01a6..cdb019484f 100644 --- a/docs/reference/api/containers/optioncontainer.rst +++ b/docs/reference/api/containers/optioncontainer.rst @@ -11,7 +11,7 @@ A container that can display multiple labeled tabs of content. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(OptionContainer|Component))'} diff --git a/docs/reference/api/containers/scrollcontainer.rst b/docs/reference/api/containers/scrollcontainer.rst index 15a927cb1b..f43c9dfc3a 100644 --- a/docs/reference/api/containers/scrollcontainer.rst +++ b/docs/reference/api/containers/scrollcontainer.rst @@ -12,7 +12,7 @@ overflow controlled by scroll bars. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(ScrollContainer|Component))'} Usage diff --git a/docs/reference/api/containers/splitcontainer.rst b/docs/reference/api/containers/splitcontainer.rst index f221e93f3a..53777f397a 100644 --- a/docs/reference/api/containers/splitcontainer.rst +++ b/docs/reference/api/containers/splitcontainer.rst @@ -11,7 +11,7 @@ A container that divides an area into two panels with a movable border. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(SplitContainer|Component))'} diff --git a/docs/reference/api/mainwindow.rst b/docs/reference/api/mainwindow.rst index caedcd55a4..887da05ce1 100644 --- a/docs/reference/api/mainwindow.rst +++ b/docs/reference/api/mainwindow.rst @@ -5,7 +5,7 @@ MainWindow .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(MainWindow|Component))'} A window for displaying components to the user diff --git a/docs/reference/api/resources/app_paths.rst b/docs/reference/api/resources/app_paths.rst index 92a1a69d53..a236c39341 100644 --- a/docs/reference/api/resources/app_paths.rst +++ b/docs/reference/api/resources/app_paths.rst @@ -8,7 +8,7 @@ application. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(App Paths|Component)$)'} Usage diff --git a/docs/reference/api/resources/command.rst b/docs/reference/api/resources/command.rst index d3a84428ac..1f05bba2c1 100644 --- a/docs/reference/api/resources/command.rst +++ b/docs/reference/api/resources/command.rst @@ -5,7 +5,7 @@ Command .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(Command|Component))'} diff --git a/docs/reference/api/resources/fonts.rst b/docs/reference/api/resources/fonts.rst index c4a7c0d272..e43ca26737 100644 --- a/docs/reference/api/resources/fonts.rst +++ b/docs/reference/api/resources/fonts.rst @@ -5,7 +5,7 @@ Font .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(Font|Component))'} The font class is used for abstracting the platforms implementation of fonts. diff --git a/docs/reference/api/resources/group.rst b/docs/reference/api/resources/group.rst index 15293f64d8..33a94ccced 100644 --- a/docs/reference/api/resources/group.rst +++ b/docs/reference/api/resources/group.rst @@ -5,7 +5,7 @@ Group .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(Group|Component))'} diff --git a/docs/reference/api/resources/icons.rst b/docs/reference/api/resources/icons.rst index fad27e5918..b241e69a2e 100644 --- a/docs/reference/api/resources/icons.rst +++ b/docs/reference/api/resources/icons.rst @@ -5,7 +5,7 @@ Icon .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(Icon|Component))'} diff --git a/docs/reference/api/resources/images.rst b/docs/reference/api/resources/images.rst index c3e8720681..dfb4e88401 100644 --- a/docs/reference/api/resources/images.rst +++ b/docs/reference/api/resources/images.rst @@ -5,7 +5,7 @@ Image .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(Image|Component)$)'} diff --git a/docs/reference/api/widgets/activityindicator.rst b/docs/reference/api/widgets/activityindicator.rst index 64eb04c641..a8d831af0c 100644 --- a/docs/reference/api/widgets/activityindicator.rst +++ b/docs/reference/api/widgets/activityindicator.rst @@ -11,7 +11,7 @@ usually rendered as a "spinner" animation. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(ActivityIndicator|Component)$)'} Usage diff --git a/docs/reference/api/widgets/button.rst b/docs/reference/api/widgets/button.rst index 27697799d3..7d4281ef9b 100644 --- a/docs/reference/api/widgets/button.rst +++ b/docs/reference/api/widgets/button.rst @@ -11,7 +11,7 @@ A button that can be pressed or clicked. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(Button|Component))'} Usage diff --git a/docs/reference/api/widgets/canvas.rst b/docs/reference/api/widgets/canvas.rst index 9158f58c3a..28055acd1f 100644 --- a/docs/reference/api/widgets/canvas.rst +++ b/docs/reference/api/widgets/canvas.rst @@ -5,7 +5,7 @@ Canvas .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(Canvas|Component))'} The canvas is used for creating a blank widget that you can draw on. diff --git a/docs/reference/api/widgets/dateinput.rst b/docs/reference/api/widgets/dateinput.rst index 75a61d1497..1589ddebf8 100644 --- a/docs/reference/api/widgets/dateinput.rst +++ b/docs/reference/api/widgets/dateinput.rst @@ -11,7 +11,7 @@ A widget to select a calendar date. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(DateInput|Component))'} Usage diff --git a/docs/reference/api/widgets/detailedlist.rst b/docs/reference/api/widgets/detailedlist.rst index 6a5c2128e9..feaad0d995 100644 --- a/docs/reference/api/widgets/detailedlist.rst +++ b/docs/reference/api/widgets/detailedlist.rst @@ -5,7 +5,7 @@ DetailedList .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(DetailedList|Component))'} diff --git a/docs/reference/api/widgets/divider.rst b/docs/reference/api/widgets/divider.rst index de8d016b3d..d5f26c65ff 100644 --- a/docs/reference/api/widgets/divider.rst +++ b/docs/reference/api/widgets/divider.rst @@ -10,7 +10,7 @@ A separator used to visually distinguish two sections of content in a layout. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(Divider|Component))'} Usage diff --git a/docs/reference/api/widgets/imageview.rst b/docs/reference/api/widgets/imageview.rst index 468e2ea59c..f76474a1e0 100644 --- a/docs/reference/api/widgets/imageview.rst +++ b/docs/reference/api/widgets/imageview.rst @@ -5,7 +5,7 @@ ImageView .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(ImageView|Component)$)'} A widget that displays an image. diff --git a/docs/reference/api/widgets/label.rst b/docs/reference/api/widgets/label.rst index eab5000759..42342df059 100644 --- a/docs/reference/api/widgets/label.rst +++ b/docs/reference/api/widgets/label.rst @@ -10,7 +10,7 @@ A text label for annotating forms or interfaces. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(Label|Component)$)'} Usage diff --git a/docs/reference/api/widgets/multilinetextinput.rst b/docs/reference/api/widgets/multilinetextinput.rst index a7d5b6fa4a..f3bd067db8 100644 --- a/docs/reference/api/widgets/multilinetextinput.rst +++ b/docs/reference/api/widgets/multilinetextinput.rst @@ -11,7 +11,7 @@ A scrollable panel that allows for the display and editing of multiple lines of .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(MultilineTextInput|Component)$)'} Usage diff --git a/docs/reference/api/widgets/numberinput.rst b/docs/reference/api/widgets/numberinput.rst index d149c7ed37..3f67e169de 100644 --- a/docs/reference/api/widgets/numberinput.rst +++ b/docs/reference/api/widgets/numberinput.rst @@ -10,7 +10,7 @@ A text input that is limited to numeric input. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(NumberInput|Component)$)'} Usage diff --git a/docs/reference/api/widgets/passwordinput.rst b/docs/reference/api/widgets/passwordinput.rst index 04bb125095..5cfb927910 100644 --- a/docs/reference/api/widgets/passwordinput.rst +++ b/docs/reference/api/widgets/passwordinput.rst @@ -13,7 +13,7 @@ not the actual characters. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(PasswordInput|Component)$)'} diff --git a/docs/reference/api/widgets/progressbar.rst b/docs/reference/api/widgets/progressbar.rst index ec00785680..b64b02439f 100644 --- a/docs/reference/api/widgets/progressbar.rst +++ b/docs/reference/api/widgets/progressbar.rst @@ -11,7 +11,7 @@ known or indeterminate length. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(ProgressBar|Component)$)'} Usage diff --git a/docs/reference/api/widgets/selection.rst b/docs/reference/api/widgets/selection.rst index 0fcb349b3c..49137c19e4 100644 --- a/docs/reference/api/widgets/selection.rst +++ b/docs/reference/api/widgets/selection.rst @@ -10,7 +10,7 @@ A widget to select a single option from a list of alternatives. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(Selection|Component)$)'} diff --git a/docs/reference/api/widgets/slider.rst b/docs/reference/api/widgets/slider.rst index 9f6b54fcf0..18e7db239b 100644 --- a/docs/reference/api/widgets/slider.rst +++ b/docs/reference/api/widgets/slider.rst @@ -12,7 +12,7 @@ and the selected value is shown as a draggable marker. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(Slider|Component)$)'} diff --git a/docs/reference/api/widgets/switch.rst b/docs/reference/api/widgets/switch.rst index fec2d81f95..cb019004e1 100644 --- a/docs/reference/api/widgets/switch.rst +++ b/docs/reference/api/widgets/switch.rst @@ -12,7 +12,7 @@ unchecked). The button has a text label. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(Switch|Component)$)'} Usage diff --git a/docs/reference/api/widgets/table.rst b/docs/reference/api/widgets/table.rst index dc1cd3e433..60551accb8 100644 --- a/docs/reference/api/widgets/table.rst +++ b/docs/reference/api/widgets/table.rst @@ -5,7 +5,7 @@ Table .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(Table|Component)$)'} The table widget is a widget for displaying tabular data. It can be instantiated with the list of headings and then data rows diff --git a/docs/reference/api/widgets/textinput.rst b/docs/reference/api/widgets/textinput.rst index 30fa1ba7f4..e2f9db68bc 100644 --- a/docs/reference/api/widgets/textinput.rst +++ b/docs/reference/api/widgets/textinput.rst @@ -11,7 +11,7 @@ A widget for the display and editing of a single line of text. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(TextInput|Component)$)'} Usage diff --git a/docs/reference/api/widgets/timeinput.rst b/docs/reference/api/widgets/timeinput.rst index 03f064de16..13500a99b9 100644 --- a/docs/reference/api/widgets/timeinput.rst +++ b/docs/reference/api/widgets/timeinput.rst @@ -11,7 +11,7 @@ A widget to select a clock time. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(TimeInput|Component))'} Usage diff --git a/docs/reference/api/widgets/tree.rst b/docs/reference/api/widgets/tree.rst index a3703da2c2..16b046e9bc 100644 --- a/docs/reference/api/widgets/tree.rst +++ b/docs/reference/api/widgets/tree.rst @@ -5,7 +5,7 @@ Tree .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(Tree|Component)$)'} The tree widget is still under development. diff --git a/docs/reference/api/widgets/webview.rst b/docs/reference/api/widgets/webview.rst index 15cef387d1..9ebdd05f0f 100644 --- a/docs/reference/api/widgets/webview.rst +++ b/docs/reference/api/widgets/webview.rst @@ -10,7 +10,7 @@ An embedded web browser. .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(WebView|Component)$)'} Usage diff --git a/docs/reference/api/widgets/widget.rst b/docs/reference/api/widgets/widget.rst index ce5e873790..51c1162470 100644 --- a/docs/reference/api/widgets/widget.rst +++ b/docs/reference/api/widgets/widget.rst @@ -7,7 +7,7 @@ The abstract base class of all widgets. This class should not be be instantiated .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!^(Widget|Component)$)'} diff --git a/docs/reference/api/window.rst b/docs/reference/api/window.rst index 0b02584aaa..a494f7d764 100644 --- a/docs/reference/api/window.rst +++ b/docs/reference/api/window.rst @@ -5,7 +5,7 @@ Window .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 :file: ../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9 + :included_cols: 4,5,6,7,8,9,10 :exclude: {0: '(?!(Window|Component))'} A window for displaying components to the user diff --git a/docs/reference/data/widgets_by_platform.csv b/docs/reference/data/widgets_by_platform.csv index 77228f0e74..7ab7e8b379 100644 --- a/docs/reference/data/widgets_by_platform.csv +++ b/docs/reference/data/widgets_by_platform.csv @@ -1,35 +1,35 @@ -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|,,,,|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|, -DetailedList,General Widget,:class:`~toga.DetailedList`,A list of complex content,|b|,|b|,,|b|,|b|, -Divider,General Widget,:class:`~toga.Divider`,A horizontal or vertical line,|y|,|y|,|y|,,,|b| -ImageView,General Widget,:class:`~toga.ImageView`,A widget that displays an image,|y|,|y|,|y|,|y|,|y|, -Label,General Widget,:class:`~toga.Label`,Text label,|y|,|y|,|y|,|y|,|y|,|b| -MultilineTextInput,General Widget,:class:`~toga.MultilineTextInput`,Multi-line Text Input field,|y|,|y|,|y|,|y|,|y|, -NumberInput,General Widget,:class:`~toga.NumberInput`,A text input that is limited to numeric input,|y|,|y|,|y|,|y|,|y|, -PasswordInput,General Widget,:class:`~toga.PasswordInput`,A text input that hides its input,|y|,|y|,|y|,|y|,|y|, -ProgressBar,General Widget,:class:`~toga.ProgressBar`,Progress Bar,|y|,|y|,|y|,|y|,|y|,|b| -Selection,General Widget,:class:`~toga.Selection`,A widget to select a single option from a list of alternatives.,|y|,|y|,|y|,|y|,|y|, -Slider,General Widget,:class:`~toga.Slider`,Slider,|y|,|y|,|y|,|y|,|y|, -Switch,General Widget,:class:`~toga.Switch`,Switch,|y|,|y|,|y|,|y|,|y|,|b| -Table,General Widget,:class:`~toga.Table`,Table of data,|b|,|b|,|b|,,|b|, -TextInput,General Widget,:class:`~toga.TextInput`,A widget for the display and editing of a single line of text.,|y|,|y|,|y|,|y|,|y|,|b| -TimeInput,General Widget,:class:`~toga.TimeInput`,A widget to select a clock time,,,|y|,,|y|, -Tree,General Widget,:class:`~toga.Tree`,Tree of data,|b|,|b|,|b|,,, -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| -Box,Layout Widget,:class:`~toga.Box`,Container for components,|y|,|y|,|y|,|y|,|y|,|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|, -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|,,, -App Paths,Resource,:class:`~toga.paths.Paths`,A mechanism for obtaining platform-appropriate filesystem locations for an application.,|y|,|y|,|y|,|y|,|y|, -Font,Resource,:class:`~toga.Font`,Fonts,|b|,|b|,|b|,|b|,|b|, -Command,Resource,:class:`~toga.Command`,Command,|b|,|b|,|b|,,|b|, -Group,Resource,:class:`~toga.Group`,Command group,|b|,|b|,|b|,|b|,|b|, -Icon,Resource,:class:`~toga.Icon`,"An icon for buttons, menus, etc",|b|,|b|,|b|,|b|,|b|, -Image,Resource,:class:`~toga.Image`,An image,|y|,|y|,|y|,|y|,|y|, +Component,Type,Component,Description,macOS,GTK,Windows,iOS,Android,Web,Terminal +Application,Core Component,:class:`~toga.App`,The application itself,|b|,|b|,|b|,|b|,|b|,|b|,|b| +Window,Core Component,:class:`~toga.Window`,Window object,|b|,|b|,|b|,|b|,|b|,|b|,|b| +MainWindow,Core Component,:class:`~toga.MainWindow`,Main window of the application,|b|,|b|,|b|,|b|,|b|,|b|,|b| +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|,|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|,, +DetailedList,General Widget,:class:`~toga.DetailedList`,A list of complex content,|b|,|b|,,|b|,|b|,, +Divider,General Widget,:class:`~toga.Divider`,A horizontal or vertical line,|y|,|y|,|y|,,,|b|, +ImageView,General Widget,:class:`~toga.ImageView`,A widget that displays an image,|y|,|y|,|y|,|y|,|y|,, +Label,General Widget,:class:`~toga.Label`,Text label,|y|,|y|,|y|,|y|,|y|,|b|,|b| +MultilineTextInput,General Widget,:class:`~toga.MultilineTextInput`,Multi-line Text Input field,|y|,|y|,|y|,|y|,|y|,, +NumberInput,General Widget,:class:`~toga.NumberInput`,A text input that is limited to numeric input,|y|,|y|,|y|,|y|,|y|,, +PasswordInput,General Widget,:class:`~toga.PasswordInput`,A text input that hides its input,|y|,|y|,|y|,|y|,|y|,, +ProgressBar,General Widget,:class:`~toga.ProgressBar`,Progress Bar,|y|,|y|,|y|,|y|,|y|,|b|, +Selection,General Widget,:class:`~toga.Selection`,A widget to select a single option from a list of alternatives.,|y|,|y|,|y|,|y|,|y|,, +Slider,General Widget,:class:`~toga.Slider`,Slider,|y|,|y|,|y|,|y|,|y|,, +Switch,General Widget,:class:`~toga.Switch`,Switch,|y|,|y|,|y|,|y|,|y|,|b|, +Table,General Widget,:class:`~toga.Table`,Table of data,|b|,|b|,|b|,,|b|,, +TextInput,General Widget,:class:`~toga.TextInput`,A widget for the display and editing of a single line of text.,|y|,|y|,|y|,|y|,|y|,|b|,|b| +TimeInput,General Widget,:class:`~toga.TimeInput`,A widget to select a clock time,,,|y|,,|y|,, +Tree,General Widget,:class:`~toga.Tree`,Tree of data,|b|,|b|,|b|,,,, +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|,, +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|,,,, +App Paths,Resource,:class:`~toga.paths.Paths`,A mechanism for obtaining platform-appropriate filesystem locations for an application.,|y|,|y|,|y|,|y|,|y|,,|b| +Font,Resource,:class:`~toga.Font`,Fonts,|b|,|b|,|b|,|b|,|b|,, +Command,Resource,:class:`~toga.Command`,Command,|b|,|b|,|b|,,|b|,, +Group,Resource,:class:`~toga.Group`,Command group,|b|,|b|,|b|,|b|,|b|,, +Icon,Resource,:class:`~toga.Icon`,"An icon for buttons, menus, etc",|b|,|b|,|b|,|b|,|b|,,|b| +Image,Resource,:class:`~toga.Image`,An image,|y|,|y|,|y|,|y|,|y|,, diff --git a/docs/reference/platforms/index.rst b/docs/reference/platforms/index.rst index 46cb5de1de..55de362d3c 100644 --- a/docs/reference/platforms/index.rst +++ b/docs/reference/platforms/index.rst @@ -24,11 +24,11 @@ Mobile Other ===== - .. toctree:: :maxdepth: 1 web + terminal testing Future Plans @@ -40,7 +40,6 @@ Eventually, the Toga project would like to provide support for: * Qt (for KDE-based Unix desktops) * tvOS (for AppleTV devices) * watchOS (for Apple Watch devices) -* Curses/Textual (for console apps) If you are interested in these platforms and would like to contribute, please get in touch on `Mastodon `__ or `Discord diff --git a/docs/reference/platforms/terminal.rst b/docs/reference/platforms/terminal.rst new file mode 100644 index 0000000000..07f847e851 --- /dev/null +++ b/docs/reference/platforms/terminal.rst @@ -0,0 +1,36 @@ +======== +Terminal +======== + +.. .. image:: /reference/screenshots/terminal.png +.. :align: center +.. :width: 300 + +The Toga backend for terminal applications is `toga-textual +`__. + +Prerequisites +------------- + +``toga-textual`` should run on any terminal or command shell provided by macOS, Windows +or Linux. + +Installation +------------ + +``toga-textual`` must be manually installed by running: + +.. code-block:: console + + $ python -m pip install toga-textual + +If ``toga-textual`` is the only Toga backend that is installed, it will be picked up +automatically on any desktop operating system. If you have another backend installed +(usually, this will be the default GUI for your operating system), you will need to set +the ``TOGA_BACKEND`` environment variable to ``toga-textual`` to force the selection of +the backend. + +Implementation details +---------------------- + +``toga-textual`` uses the `Textual `__ UI toolkit. diff --git a/docs/reference/widgets_by_platform.rst b/docs/reference/widgets_by_platform.rst index a5157615cd..60f35a850b 100644 --- a/docs/reference/widgets_by_platform.rst +++ b/docs/reference/widgets_by_platform.rst @@ -21,10 +21,10 @@ Core Components :file: data/widgets_by_platform.csv :header-rows: 1 :exclude: {1: '(?!(Type|Core Component))'} - :included_cols: 2,4,5,6,7,8,9 + :included_cols: 2,4,5,6,7,8,9,10 :class: longtable :stub-columns: 1 - :widths: 3 1 1 1 1 1 1 + :widths: 3 1 1 1 1 1 1 1 General Widgets @@ -35,10 +35,10 @@ General Widgets :file: data/widgets_by_platform.csv :header-rows: 1 :exclude: {1: '(?!(Type|General Widget))'} - :included_cols: 2,4,5,6,7,8,9 + :included_cols: 2,4,5,6,7,8,9,10 :class: longtable :stub-columns: 1 - :widths: 3 1 1 1 1 1 1 + :widths: 3 1 1 1 1 1 1 1 Layout Widgets ============== @@ -48,10 +48,10 @@ Layout Widgets :file: data/widgets_by_platform.csv :header-rows: 1 :exclude: {1: '(?!(Type|Layout Widget))'} - :included_cols: 2,4,5,6,7,8,9 + :included_cols: 2,4,5,6,7,8,9,10 :class: longtable :stub-columns: 1 - :widths: 3 1 1 1 1 1 1 + :widths: 3 1 1 1 1 1 1 1 Resources ========= @@ -61,7 +61,7 @@ Resources :file: data/widgets_by_platform.csv :header-rows: 1 :exclude: {1: '(?!(Type|Resource))'} - :included_cols: 2,4,5,6,7,8,9 + :included_cols: 2,4,5,6,7,8,9,10 :class: longtable :stub-columns: 1 - :widths: 3 1 1 1 1 1 1 + :widths: 3 1 1 1 1 1 1 1 diff --git a/nursery/textual/tests/test_implementation.py b/nursery/textual/tests/test_implementation.py deleted file mode 100644 index d5e8f05f58..0000000000 --- a/nursery/textual/tests/test_implementation.py +++ /dev/null @@ -1,11 +0,0 @@ -import os - -from toga_dummy import test_implementation - -globals().update( - test_implementation.create_impl_tests( - os.path.abspath( - os.path.join(os.path.dirname(os.path.dirname(__file__)), "toga_textual") - ) - ) -) diff --git a/nursery/textual/toga_textual/__init__.py b/nursery/textual/toga_textual/__init__.py deleted file mode 100644 index e889ffaa5c..0000000000 --- a/nursery/textual/toga_textual/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Examples of valid version strings -# __version__ = '1.2.3.dev1' # Development release 1 -# __version__ = '1.2.3a1' # Alpha Release 1 -# __version__ = '1.2.3b1' # Beta Release 1 -# __version__ = '1.2.3rc1' # RC Release 1 -# __version__ = '1.2.3' # Final Release -# __version__ = '1.2.3.post1' # Post Release 1 - -__version__ = "0.0.0" diff --git a/nursery/textual/CONTRIBUTING.md b/textual/CONTRIBUTING.md similarity index 100% rename from nursery/textual/CONTRIBUTING.md rename to textual/CONTRIBUTING.md diff --git a/nursery/textual/MANIFEST.in b/textual/MANIFEST.in similarity index 100% rename from nursery/textual/MANIFEST.in rename to textual/MANIFEST.in diff --git a/nursery/textual/README.rst b/textual/README.rst similarity index 78% rename from nursery/textual/README.rst rename to textual/README.rst index b9a4df8a42..26913d1e91 100644 --- a/nursery/textual/README.rst +++ b/textual/README.rst @@ -3,17 +3,14 @@ toga-textual A Textual backend for the `Toga widget toolkit`_. -**Toga requires Python 3** - -**THIS IS A PLACEHOLDER PROJECT** - -At present, it has no functionality - it exists purely to reserve the PyPI namespace. - This package isn't much use by itself; it needs to be combined with `the core Toga library`_. +For platform requirements, see the `Terminal platform documentation +`__. + For more details, see the `Toga project on Github`_. -.. _Toga widget toolkit: http://beeware.org/toga +.. _Toga widget toolkit: https://beeware.org/toga .. _the core Toga library: https://pypi.python.org/pypi/toga-core .. _Toga project on Github: https://github.com/beeware/toga @@ -29,11 +26,11 @@ Toga is part of the `BeeWare suite`_. You can talk to the community through: We foster a welcoming and respectful community as described in our `BeeWare Community Code of Conduct`_. -.. _BeeWare suite: http://beeware.org +.. _BeeWare suite: https://beeware.org .. _@beeware@fosstodon.org on Mastodon: https://fosstodon.org/@beeware .. _Discord: https://beeware.org/bee/chat/ .. _Github Discussions forum: https://github.com/beeware/toga/discussions -.. _BeeWare Community Code of Conduct: http://beeware.org/community/behavior/ +.. _BeeWare Community Code of Conduct: https://beeware.org/community/behavior/ Contributing ------------ diff --git a/nursery/textual/pyproject.toml b/textual/pyproject.toml similarity index 100% rename from nursery/textual/pyproject.toml rename to textual/pyproject.toml diff --git a/nursery/textual/setup.cfg b/textual/setup.cfg similarity index 84% rename from nursery/textual/setup.cfg rename to textual/setup.cfg index 170e45ca1d..2b98f712cb 100644 --- a/nursery/textual/setup.cfg +++ b/textual/setup.cfg @@ -27,7 +27,8 @@ classifiers = Topic :: Software Development :: User Interfaces Topic :: Software Development :: Widget Sets license = New BSD -license_file = LICENSE +license_files = + = LICENSE long_description = file: README.rst long_description_content_type = text/x-rst; charset=UTF-8 keywords = @@ -41,25 +42,18 @@ keywords = [options] packages = find: +package_dir = + = src python_requires = >= 3.8 zip_safe = False -[options.packages.find] -include = - toga_textual - toga_textual.* - [options.entry_points] toga.backends = - console = toga_textual + # The textual backend is a candidate for use on every platform. + linux = toga_textual + windows = toga_textual + macOS = toga_textual + freeBSD = toga_textual -[flake8] -exclude=\ - .eggs/*,\ - build/* -max-complexity = 10 -max-line-length = 119 -ignore = E121,E123,E126,E226,E24,E704,W503,W504,C901 - -[isort] -multi_line_output = 3 +[options.packages.find] +where = src diff --git a/nursery/textual/setup.py b/textual/setup.py similarity index 76% rename from nursery/textual/setup.py rename to textual/setup.py index 511a59336d..1977b3cf11 100644 --- a/nursery/textual/setup.py +++ b/textual/setup.py @@ -1,11 +1,12 @@ from setuptools import setup from setuptools_scm import get_version -version = get_version(root="../..") +version = get_version(root="..") setup( version=version, install_requires=[ f"toga-core == {version}", + "textual", ], ) diff --git a/textual/src/toga_textual/__init__.py b/textual/src/toga_textual/__init__.py new file mode 100644 index 0000000000..80aa53f6aa --- /dev/null +++ b/textual/src/toga_textual/__init__.py @@ -0,0 +1,3 @@ +import toga + +__version__ = toga._package_version(__file__, __name__) diff --git a/textual/src/toga_textual/app.py b/textual/src/toga_textual/app.py new file mode 100644 index 0000000000..923db033d5 --- /dev/null +++ b/textual/src/toga_textual/app.py @@ -0,0 +1,71 @@ +import asyncio + +from textual.app import App as TextualApp + +from .window import Window + + +class MainWindow(Window): + pass + + +class TogaApp(TextualApp): + def __init__(self, impl): + super().__init__() + self.interface = impl.interface + self.impl = impl + + def on_mount(self) -> None: + self.impl.create() + + +class App: + def __init__(self, interface): + self.interface = interface + self.loop = asyncio.new_event_loop() + self.native = TogaApp(self) + + def create(self): + self.interface._startup() + self.set_current_window(self.interface.main_window._impl) + + def create_menus(self): + pass + + def main_loop(self): + self.native.run() + + def set_main_window(self, window): + self.native.push_screen(self.interface.main_window.id) + + def show_about_dialog(self): + pass + + def beep(self): + self.native.bell() + + def exit(self): + self.native.exit() + + def get_current_window(self): + pass + + def set_current_window(self, window): + self.native.switch_screen(window.native) + self.native.title = window.get_title() + + def enter_full_screen(self, windows): + pass + + def exit_full_screen(self, windows): + pass + + def show_cursor(self): + pass + + def hide_cursor(self): + pass + + +class DocumentApp(App): + pass diff --git a/textual/src/toga_textual/container.py b/textual/src/toga_textual/container.py new file mode 100644 index 0000000000..9999f92080 --- /dev/null +++ b/textual/src/toga_textual/container.py @@ -0,0 +1,39 @@ +class Container: + def __init__(self, on_refresh=None): + self._content = None + self.on_refresh = on_refresh + + # FIXME... + self.dpi = 96 + self.baseline_dpi = self.dpi + + @property + def content(self): + return self._content + + @content.setter + def content(self, widget): + if self._content: + self._content.container = None + + self._content = widget + if widget: + widget.container = self + + @property + def width(self): + if self._content: + return self.content.native.size.width + else: + return 0 + + @property + def height(self): + if self._content: + return self.content.native.size.height + else: + return 0 + + def refreshed(self): + if self.on_refresh: + self.on_refresh() diff --git a/textual/src/toga_textual/factory.py b/textual/src/toga_textual/factory.py new file mode 100644 index 0000000000..5964350f06 --- /dev/null +++ b/textual/src/toga_textual/factory.py @@ -0,0 +1,89 @@ +# from . import dialogs +from .app import App, DocumentApp, MainWindow + +# from .command import Command +# from .documents import Document +# from .fonts import Font +from .icons import Icon + +# from .images import Image +from .paths import Paths + +# from .widgets.activityindicator import ActivityIndicator +# from .widgets.base import Widget +from .widgets.box import Box +from .widgets.button import Button + +# from .widgets.canvas import Canvas +# from .widgets.dateinput import DateInput +# from .widgets.detailedlist import DetailedList +# from .widgets.divider import Divider +# from .widgets.imageview import ImageView +from .widgets.label import Label + +# from .widgets.multilinetextinput import MultilineTextInput +# from .widgets.numberinput import NumberInput +# from .widgets.optioncontainer import OptionContainer +# from .widgets.passwordinput import PasswordInput +# from .widgets.progressbar import ProgressBar +# from .widgets.scrollcontainer import ScrollContainer +# from .widgets.selection import Selection +# from .widgets.slider import Slider +# from .widgets.splitcontainer import SplitContainer +# from .widgets.switch import Switch +# from .widgets.table import Table +from .widgets.textinput import TextInput + +# from .widgets.timeinput import TimeInput +# from .widgets.tree import Tree +# from .widgets.webview import WebView +from .window import Window + + +def not_implemented(feature): + print(f"[Textual] Not implemented: {feature}") # pragma: nocover + + +__all__ = [ + "not_implemented", + "App", + "DocumentApp", + "MainWindow", + # "Command", + # "Document", + # "Font", + "Icon", + # "Image", + "Paths", + # "dialogs", + # # Widgets + # "ActivityIndicator", + "Box", + "Button", + # "Canvas", + # "DateInput", + # "DetailedList", + # "Divider", + # "ImageView", + "Label", + # "MultilineTextInput", + # "NumberInput", + # "OptionContainer", + # "PasswordInput", + # "ProgressBar", + # "ScrollContainer", + # "Selection", + # "Slider", + # "SplitContainer", + # "Switch", + # "Table", + "TextInput", + # "TimeInput", + # "Tree", + # "WebView", + "Window", +] + + +def __getattr__(name): # pragma: no cover + raise NotImplementedError(f"Toga's Textual backend doesn't implement {name}") diff --git a/textual/src/toga_textual/icons.py b/textual/src/toga_textual/icons.py new file mode 100644 index 0000000000..ba2d1216dd --- /dev/null +++ b/textual/src/toga_textual/icons.py @@ -0,0 +1,8 @@ +class Icon: + EXTENSIONS = [".png"] + SIZES = None + + def __init__(self, interface, path): + super().__init__() + self.interface = interface + self.path = path diff --git a/textual/src/toga_textual/paths.py b/textual/src/toga_textual/paths.py new file mode 100644 index 0000000000..a93f7d0af6 --- /dev/null +++ b/textual/src/toga_textual/paths.py @@ -0,0 +1,94 @@ +import sys +from pathlib import Path + +from toga import App + +if sys.platform == "darwin": + + class Paths: + def __init__(self, interface): + self.interface = interface + + def get_config_path(self): + return Path.home() / "Library" / "Preferences" / App.app.app_id + + def get_data_path(self): + return Path.home() / "Library" / "Application Support" / App.app.app_id + + def get_cache_path(self): + return Path.home() / "Library" / "Caches" / App.app.app_id + + def get_logs_path(self): + return Path.home() / "Library" / "Logs" / App.app.app_id + +elif sys.platform == "win32": + + class Paths: + def __init__(self, interface): + self.interface = interface + + @property + def author(self): + # No coverage testing of this because we can't easily configure + # the app to have no author. + if App.app.author is None: # pragma: no cover + return "Unknown" + return App.app.author + + def get_config_path(self): + return ( + Path.home() + / "AppData" + / "Local" + / self.author + / App.app.formal_name + / "Config" + ) + + def get_data_path(self): + return ( + Path.home() + / "AppData" + / "Local" + / self.author + / App.app.formal_name + / "Data" + ) + + def get_cache_path(self): + return ( + Path.home() + / "AppData" + / "Local" + / self.author + / App.app.formal_name + / "Cache" + ) + + def get_logs_path(self): + return ( + Path.home() + / "AppData" + / "Local" + / self.author + / App.app.formal_name + / "Logs" + ) + +else: + + class Paths: + def __init__(self, interface): + self.interface = interface + + def get_config_path(self): + return Path.home() / ".config" / App.app.app_name + + def get_data_path(self): + return Path.home() / ".local" / "share" / App.app.app_name + + def get_cache_path(self): + return Path.home() / ".cache" / App.app.app_name + + def get_logs_path(self): + return Path.home() / ".local" / "state" / App.app.app_name / "log" diff --git a/nursery/textual/tests/__init__.py b/textual/src/toga_textual/widgets/__init__.py similarity index 100% rename from nursery/textual/tests/__init__.py rename to textual/src/toga_textual/widgets/__init__.py diff --git a/textual/src/toga_textual/widgets/base.py b/textual/src/toga_textual/widgets/base.py new file mode 100644 index 0000000000..7f648e6241 --- /dev/null +++ b/textual/src/toga_textual/widgets/base.py @@ -0,0 +1,82 @@ +class Widget: + def __init__(self, interface): + self.interface = interface + self.interface._impl = self + self.container = None + self.create() + + @property + def viewport(self): + return self.container + + def get_size(self): + return (0, 0) + + def create(self): + pass + + def set_app(self, app): + pass + + def set_window(self, window): + pass + + def get_enabled(self): + return not self.native.disabled + + def set_enabled(self, value): + self.native.disabled = not value + + def focus(self): + pass + + def get_tab_index(self): + return None + + def set_tab_index(self, tab_index): + pass + + ###################################################################### + # APPLICATOR + ###################################################################### + + def set_bounds(self, x, y, width, height): + pass + # self.native.styles.width = width + # self.native.styles.height = height + + def set_alignment(self, alignment): + pass + + def set_hidden(self, hidden): + pass + + def set_font(self, font): + pass + + def set_color(self, color): + pass + + def set_background_color(self, color): + pass + + ###################################################################### + # INTERFACE + ###################################################################### + + def add_child(self, child): + self.native.mount(child.native) + + def insert_child(self, index, child): + pass + + def remove_child(self, child): + self.native.remove(child.native) + + def refresh(self): + pass + + def mount(self, parent): + parent.native.mount(self.native) + for child in self.interface.children: + child._impl.mount(self) diff --git a/textual/src/toga_textual/widgets/box.py b/textual/src/toga_textual/widgets/box.py new file mode 100644 index 0000000000..64fd52b9fa --- /dev/null +++ b/textual/src/toga_textual/widgets/box.py @@ -0,0 +1,16 @@ +from textual.containers import Container as TextualContainer +from toga.style.pack import ROW + +from .base import Widget + + +class Box(Widget): + def create(self): + self.native = TextualContainer() + + def set_bounds(self, x, y, width, height): + if self.interface.style.direction == ROW: + self.native.styles.layout = "horizontal" + else: + self.native.styles.layout = "vertical" + self.native.refresh() diff --git a/textual/src/toga_textual/widgets/button.py b/textual/src/toga_textual/widgets/button.py new file mode 100644 index 0000000000..10b2337fdf --- /dev/null +++ b/textual/src/toga_textual/widgets/button.py @@ -0,0 +1,24 @@ +from textual.widgets import Button as TextualButton + +from .base import Widget + + +class TogaButton(TextualButton): + def __init__(self, impl): + super().__init__() + self.interface = impl.interface + self.impl = impl + + def on_press(self, event: TextualButton.Pressed) -> None: + self.interface.on_press(None) + + +class Button(Widget): + def create(self): + self.native = TogaButton(self) + + def get_text(self): + return self.native.text + + def set_text(self, text): + self.native.label = text diff --git a/textual/src/toga_textual/widgets/label.py b/textual/src/toga_textual/widgets/label.py new file mode 100644 index 0000000000..684d812333 --- /dev/null +++ b/textual/src/toga_textual/widgets/label.py @@ -0,0 +1,14 @@ +from textual.widgets import Label as TextualLabel + +from .base import Widget + + +class Label(Widget): + def create(self): + self.native = TextualLabel() + + def get_text(self): + return self.native.renderable + + def set_text(self, value): + self.native.renderable = value diff --git a/textual/src/toga_textual/widgets/textinput.py b/textual/src/toga_textual/widgets/textinput.py new file mode 100644 index 0000000000..b9585202c2 --- /dev/null +++ b/textual/src/toga_textual/widgets/textinput.py @@ -0,0 +1,43 @@ +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 + + +class TextInput(Widget): + def create(self): + self.native = TogaInput(self) + self.native.styles.width = 20 + + def get_readonly(self): + return False + + def set_readonly(self, value): + pass + + def get_placeholder(self): + return self.native.placeholder + + def set_placeholder(self, value): + self.native.placeholder = value + + def get_value(self): + return self.native.value + + def set_value(self, value): + self.native.value = value + + def set_error(self, error_message): + pass + + def clear_error(self): + pass + + def is_valid(self): + return True diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py new file mode 100644 index 0000000000..fad3d0bc4f --- /dev/null +++ b/textual/src/toga_textual/window.py @@ -0,0 +1,74 @@ +from textual.screen import Screen as TextualScreen +from textual.widgets import Button as TextualButton, Header as TextualHeader + +from .container import Container + + +class TogaWindow(TextualScreen): + def __init__(self, impl): + super().__init__() + self.interface = impl.interface + self.impl = impl + + def on_mount(self) -> None: + self.mount(TextualHeader()) + + # Textual catches events at the level of the Screen/Window; + # redirect to the widget instance. + def on_button_pressed(self, event: TextualButton.Pressed) -> None: + event.button.on_press(event) + + +class Window: + def __init__(self, interface, title, position, size): + self.interface = interface + self.native = TogaWindow(self) + self.container = Container() + self.set_title(title) + + def create_toolbar(self): + pass + + def clear_content(self): + pass + + def set_content(self, widget): + self.container.content = widget + + widget.mount(self) + + def get_title(self): + return self._title + + def set_title(self, title): + self._title = title + + def get_position(self): + return (0, 0) + + def set_position(self, position): + pass + + def get_size(self): + return (80, 25) + + def set_size(self, size): + pass + + def set_app(self, app): + app.native.install_screen(self.native, name=self.interface.id) + + def show(self): + pass + + def hide(self): + pass + + def get_visible(self): + return True + + def close(self): + pass + + def set_full_screen(self, is_full_screen): + pass diff --git a/textual/tests_backend/__init__.py b/textual/tests_backend/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/textual/tests_backend/app.py b/textual/tests_backend/app.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/textual/tests_backend/images.py b/textual/tests_backend/images.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/textual/tests_backend/probe.py b/textual/tests_backend/probe.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/textual/tests_backend/widgets/__init__.py b/textual/tests_backend/widgets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/textual/tests_backend/widgets/base.py b/textual/tests_backend/widgets/base.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/textual/tests_backend/widgets/box.py b/textual/tests_backend/widgets/box.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/textual/tests_backend/widgets/button.py b/textual/tests_backend/widgets/button.py new file mode 100644 index 0000000000..e69de29bb2 From a90f1ae49cf0bd83f41a76d433e6b3abdda74c9d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 7 Aug 2023 09:00:55 +0800 Subject: [PATCH 02/10] Simplify widget installation at time of creation. --- textual/src/toga_textual/widgets/base.py | 5 ----- textual/src/toga_textual/window.py | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/textual/src/toga_textual/widgets/base.py b/textual/src/toga_textual/widgets/base.py index 7f648e6241..23582a81c3 100644 --- a/textual/src/toga_textual/widgets/base.py +++ b/textual/src/toga_textual/widgets/base.py @@ -75,8 +75,3 @@ def remove_child(self, child): def refresh(self): pass - - def mount(self, parent): - parent.native.mount(self.native) - for child in self.interface.children: - child._impl.mount(self) diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index fad3d0bc4f..6274183c3f 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -35,7 +35,7 @@ def clear_content(self): def set_content(self, widget): self.container.content = widget - widget.mount(self) + self.native.mount(widget.native) def get_title(self): return self._title From f8e35bb8465e2ef8fc04c50eec9cec0b863b903e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 7 Aug 2023 09:12:28 +0800 Subject: [PATCH 03/10] Add documentation note about macOS terminal issues. --- docs/reference/platforms/terminal.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/reference/platforms/terminal.rst b/docs/reference/platforms/terminal.rst index 07f847e851..54174e54b5 100644 --- a/docs/reference/platforms/terminal.rst +++ b/docs/reference/platforms/terminal.rst @@ -34,3 +34,13 @@ Implementation details ---------------------- ``toga-textual`` uses the `Textual `__ UI toolkit. + +macOS Terminal.app limitations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are some `known issues with the default macOS Terminal.app +`__. +In some layouts, box outlines render badly; this can *sometimes* be resolved by altering +the line spacing of the font used in the terminal. The default Terminal.app also has a +limited color palette. The maintainers of Textual recommend using an alternative +terminal application to avoid these problems. From bd5c5f6cb1238aaab16f164fdbfc9b9ec8755548 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 7 Aug 2023 09:16:15 +0800 Subject: [PATCH 04/10] Map textinput readonly to disabled. --- textual/src/toga_textual/widgets/textinput.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/textual/src/toga_textual/widgets/textinput.py b/textual/src/toga_textual/widgets/textinput.py index b9585202c2..07bb95c618 100644 --- a/textual/src/toga_textual/widgets/textinput.py +++ b/textual/src/toga_textual/widgets/textinput.py @@ -13,13 +13,12 @@ def __init__(self, impl): class TextInput(Widget): def create(self): self.native = TogaInput(self) - self.native.styles.width = 20 def get_readonly(self): - return False + return self.native.disabled def set_readonly(self, value): - pass + self.native.disabled = value def get_placeholder(self): return self.native.placeholder From f8a9413fab89a645b53ec40aabf852775d8e5459 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 7 Aug 2023 19:35:55 +0800 Subject: [PATCH 05/10] Simplify button event handling. --- textual/src/toga_textual/widgets/button.py | 2 +- textual/src/toga_textual/window.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/textual/src/toga_textual/widgets/button.py b/textual/src/toga_textual/widgets/button.py index 10b2337fdf..6d6dc5c913 100644 --- a/textual/src/toga_textual/widgets/button.py +++ b/textual/src/toga_textual/widgets/button.py @@ -9,7 +9,7 @@ def __init__(self, impl): self.interface = impl.interface self.impl = impl - def on_press(self, event: TextualButton.Pressed) -> None: + def on_button_pressed(self, event: TextualButton.Pressed) -> None: self.interface.on_press(None) diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index 6274183c3f..1fde765dd5 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -1,5 +1,5 @@ from textual.screen import Screen as TextualScreen -from textual.widgets import Button as TextualButton, Header as TextualHeader +from textual.widgets import Header as TextualHeader from .container import Container @@ -13,11 +13,6 @@ def __init__(self, impl): def on_mount(self) -> None: self.mount(TextualHeader()) - # Textual catches events at the level of the Screen/Window; - # redirect to the widget instance. - def on_button_pressed(self, event: TextualButton.Pressed) -> None: - event.button.on_press(event) - class Window: def __init__(self, interface, title, position, size): From 54f9fc37837ab15aa2ed32c76621ed0a690e9538 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 7 Aug 2023 19:36:08 +0800 Subject: [PATCH 06/10] Add textinput event handlers. --- textual/src/toga_textual/widgets/textinput.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/textual/src/toga_textual/widgets/textinput.py b/textual/src/toga_textual/widgets/textinput.py index 07bb95c618..24477830cc 100644 --- a/textual/src/toga_textual/widgets/textinput.py +++ b/textual/src/toga_textual/widgets/textinput.py @@ -9,6 +9,15 @@ def __init__(self, impl): self.interface = impl.interface self.impl = impl + def on_focus(self, event: TextualInput.Changed) -> None: + self.interface.on_gain_focus(None) + + def on_input_changed(self, event: TextualInput.Changed) -> None: + self.interface.on_change(None) + + def on_input_submitted(self, event: TextualInput.Submitted) -> None: + self.interface.on_submit(None) + class TextInput(Widget): def create(self): From 1969b6265532641f72a577510921851103f17783 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 16 Aug 2023 10:00:46 +0800 Subject: [PATCH 07/10] First steps toward applying styles. --- examples/tutorial1/tutorial/app.py | 2 +- textual/src/toga_textual/container.py | 22 +++--- textual/src/toga_textual/widgets/base.py | 70 ++++++++++++++++--- textual/src/toga_textual/widgets/box.py | 8 ++- textual/src/toga_textual/widgets/button.py | 16 ++++- textual/src/toga_textual/widgets/label.py | 6 ++ textual/src/toga_textual/widgets/textinput.py | 16 ++++- textual/src/toga_textual/window.py | 7 +- 8 files changed, 118 insertions(+), 29 deletions(-) diff --git a/examples/tutorial1/tutorial/app.py b/examples/tutorial1/tutorial/app.py index 4cccb0f03a..38a71d6856 100644 --- a/examples/tutorial1/tutorial/app.py +++ b/examples/tutorial1/tutorial/app.py @@ -41,7 +41,7 @@ def calculate(widget): f_input.style.update(flex=1, padding_left=160) c_label.style.update(width=100, padding_left=10) f_label.style.update(width=100, padding_left=10) - join_label.style.update(width=150, padding_right=10) + join_label.style.update(width=200, padding_right=10) button.style.update(padding=15) diff --git a/textual/src/toga_textual/container.py b/textual/src/toga_textual/container.py index 9999f92080..fc2764e486 100644 --- a/textual/src/toga_textual/container.py +++ b/textual/src/toga_textual/container.py @@ -1,12 +1,12 @@ -class Container: - def __init__(self, on_refresh=None): +from toga_textual.widgets.base import Scalable + + +class Container(Scalable): + def __init__(self, native_parent, on_refresh=None): + self.native_parent = native_parent self._content = None self.on_refresh = on_refresh - # FIXME... - self.dpi = 96 - self.baseline_dpi = self.dpi - @property def content(self): return self._content @@ -22,17 +22,11 @@ def content(self, widget): @property def width(self): - if self._content: - return self.content.native.size.width - else: - return 0 + return self.scale_out_horizontal(self.native_parent.size[0]) @property def height(self): - if self._content: - return self.content.native.size.height - else: - return 0 + return self.scale_out_vertical(self.native_parent.size[1]) def refreshed(self): if self.on_refresh: diff --git a/textual/src/toga_textual/widgets/base.py b/textual/src/toga_textual/widgets/base.py index 23582a81c3..16879fd834 100644 --- a/textual/src/toga_textual/widgets/base.py +++ b/textual/src/toga_textual/widgets/base.py @@ -1,14 +1,38 @@ -class Widget: +from travertino.size import at_least + + +# We assume a terminal is 800x600 pixels, mapping to 80x25 characters. +# This results in an uneven scale in the horizontal and vertical directions +class Scalable: + HORIZONTAL_SCALE = 10 + VERTICAL_SCALE = 25 + + def scale_in_horizontal(self, value): + return value // self.HORIZONTAL_SCALE + + def scale_out_horizontal(self, value): + try: + return at_least(value.value * self.HORIZONTAL_SCALE) + except AttributeError: + return value * self.HORIZONTAL_SCALE + + def scale_in_vertical(self, value): + return value // self.VERTICAL_SCALE + + def scale_out_vertical(self, value): + try: + return at_least(value.value * self.VERTICAL_SCALE) + except AttributeError: + return value * self.VERTICAL_SCALE + + +class Widget(Scalable): def __init__(self, interface): self.interface = interface self.interface._impl = self self.container = None self.create() - @property - def viewport(self): - return self.container - def get_size(self): return (0, 0) @@ -40,10 +64,31 @@ def set_tab_index(self, tab_index): # APPLICATOR ###################################################################### + @property + def width_adjustment(self): + return 0 + + @property + def height_adjustment(self): + return 0 + def set_bounds(self, x, y, width, height): - pass - # self.native.styles.width = width - # self.native.styles.height = height + # Convert the width and height back into terminal coordinates, + # subtracting the extra spacing associated with the widget itself. + self.native.styles.width = ( + self.scale_in_horizontal(width) - self.width_adjustment + ) + self.native.styles.height = ( + self.scale_in_horizontal(height) - self.height_adjustment + ) + + # Apply margins to the widget based on content spacing. + # self.native.styles.margin = ( + # self.scale_in_vertical(self.interface.layout.content_top), + # self.scale_in_horizontal(self.interface.layout.content_right), + # self.scale_in_vertical(self.interface.layout.content_bottom), + # self.scale_in_horizontal(self.interface.layout.content_left), + # ) def set_alignment(self, alignment): pass @@ -74,4 +119,11 @@ def remove_child(self, child): self.native.remove(child.native) def refresh(self): - pass + intrinsic = self.interface.intrinsic + intrinsic.width = intrinsic.height = None + self.rehint() + assert intrinsic.width is not None + assert intrinsic.height is not None + + intrinsic.width = self.scale_out_horizontal(intrinsic.width) + intrinsic.height = self.scale_out_vertical(intrinsic.height) diff --git a/textual/src/toga_textual/widgets/box.py b/textual/src/toga_textual/widgets/box.py index 64fd52b9fa..a3563f9343 100644 --- a/textual/src/toga_textual/widgets/box.py +++ b/textual/src/toga_textual/widgets/box.py @@ -1,3 +1,5 @@ +from travertino.size import at_least + from textual.containers import Container as TextualContainer from toga.style.pack import ROW @@ -9,8 +11,12 @@ def create(self): self.native = TextualContainer() def set_bounds(self, x, y, width, height): + super().set_bounds(x, y, width, height) if self.interface.style.direction == ROW: self.native.styles.layout = "horizontal" else: self.native.styles.layout = "vertical" - self.native.refresh() + + def rehint(self): + self.interface.intrinsic.width = at_least(0) + self.interface.intrinsic.height = at_least(0) diff --git a/textual/src/toga_textual/widgets/button.py b/textual/src/toga_textual/widgets/button.py index 6d6dc5c913..fff220784a 100644 --- a/textual/src/toga_textual/widgets/button.py +++ b/textual/src/toga_textual/widgets/button.py @@ -1,3 +1,5 @@ +from travertino.size import at_least + from textual.widgets import Button as TextualButton from .base import Widget @@ -18,7 +20,19 @@ def create(self): self.native = TogaButton(self) def get_text(self): - return self.native.text + return self.native.label def set_text(self, text): self.native.label = text + + @property + def width_adjustment(self): + return 2 + + @property + def height_adjustment(self): + return 2 + + def rehint(self): + self.interface.intrinsic.width = at_least(len(self.native.label) + 8) + self.interface.intrinsic.height = 3 diff --git a/textual/src/toga_textual/widgets/label.py b/textual/src/toga_textual/widgets/label.py index 684d812333..2319f40fea 100644 --- a/textual/src/toga_textual/widgets/label.py +++ b/textual/src/toga_textual/widgets/label.py @@ -1,3 +1,5 @@ +from travertino.size import at_least + from textual.widgets import Label as TextualLabel from .base import Widget @@ -12,3 +14,7 @@ def get_text(self): def set_text(self, value): self.native.renderable = value + + def rehint(self): + self.interface.intrinsic.width = at_least(len(self.native.renderable)) + self.interface.intrinsic.height = 1 diff --git a/textual/src/toga_textual/widgets/textinput.py b/textual/src/toga_textual/widgets/textinput.py index 24477830cc..bedc193704 100644 --- a/textual/src/toga_textual/widgets/textinput.py +++ b/textual/src/toga_textual/widgets/textinput.py @@ -1,3 +1,5 @@ +from travertino.size import at_least + from textual.widgets import Input as TextualInput from .base import Widget @@ -16,7 +18,7 @@ def on_input_changed(self, event: TextualInput.Changed) -> None: self.interface.on_change(None) def on_input_submitted(self, event: TextualInput.Submitted) -> None: - self.interface.on_submit(None) + self.interface.on_confirm(None) class TextInput(Widget): @@ -49,3 +51,15 @@ def clear_error(self): def is_valid(self): return True + + @property + def width_adjustment(self): + return 2 + + @property + def height_adjustment(self): + return 2 + + def rehint(self): + self.interface.intrinsic.width = at_least(len(self.native.value) + 4) + self.interface.intrinsic.height = 3 diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index 1fde765dd5..65ddcc38ae 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -13,12 +13,15 @@ def __init__(self, impl): def on_mount(self) -> None: self.mount(TextualHeader()) + def on_resize(self, event) -> None: + self.interface.content.refresh() + class Window: def __init__(self, interface, title, position, size): self.interface = interface self.native = TogaWindow(self) - self.container = Container() + self.container = Container(self.native) self.set_title(title) def create_toolbar(self): @@ -45,7 +48,7 @@ def set_position(self, position): pass def get_size(self): - return (80, 25) + return (self.native.size.width, self.native.size.height) def set_size(self, size): pass From 4f492a0e681272c0ace2f4d67fdf780a96daeed1 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 16 Aug 2023 10:02:55 +0800 Subject: [PATCH 08/10] Include textual in ci release processes. --- .github/workflows/ci.yml | 1 + .github/workflows/publish.yml | 1 + .github/workflows/release.yml | 1 + textual/MANIFEST.in | 1 + 4 files changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bba4ea2b7..ea4107b426 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,7 @@ jobs: - "gtk" - "iOS" - "toga" + - "textual" - "web" - "winforms" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 681dc970d8..901edcd011 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,6 +20,7 @@ jobs: - "toga_demo" - "toga_dummy" - "toga_gtk" + - "toga_textual" - "toga_iOS" - "toga_web" - "toga_winforms" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 29f66f634e..54c05c54ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,7 @@ jobs: - "toga_dummy" - "toga_gtk" - "toga_iOS" + - "toga_textual" - "toga_web" - "toga_winforms" steps: diff --git a/textual/MANIFEST.in b/textual/MANIFEST.in index f477cdd26f..4659263f37 100644 --- a/textual/MANIFEST.in +++ b/textual/MANIFEST.in @@ -3,3 +3,4 @@ include LICENSE include README.rst recursive-include tests *.py recursive-include toga_textual *.py +recursive-include tests_backend *.py From 96305083e84da7ee00169317cc7c78815e813cc3 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 19 Aug 2023 23:32:21 +0930 Subject: [PATCH 09/10] Apply margins to widgets on layout. --- examples/tutorial1/tutorial/app.py | 2 +- textual/src/toga_textual/container.py | 3 +- textual/src/toga_textual/widgets/base.py | 87 ++++++++++++++++------ textual/src/toga_textual/widgets/box.py | 3 + textual/src/toga_textual/widgets/button.py | 8 -- textual/src/toga_textual/widgets/label.py | 2 +- 6 files changed, 71 insertions(+), 34 deletions(-) diff --git a/examples/tutorial1/tutorial/app.py b/examples/tutorial1/tutorial/app.py index 38a71d6856..ffa1854293 100644 --- a/examples/tutorial1/tutorial/app.py +++ b/examples/tutorial1/tutorial/app.py @@ -38,7 +38,7 @@ def calculate(widget): c_box.style.update(direction=ROW, padding=5) c_input.style.update(flex=1) - f_input.style.update(flex=1, padding_left=160) + f_input.style.update(flex=1, padding_left=210) c_label.style.update(width=100, padding_left=10) f_label.style.update(width=100, padding_left=10) join_label.style.update(width=200, padding_right=10) diff --git a/textual/src/toga_textual/container.py b/textual/src/toga_textual/container.py index fc2764e486..f03f5e35ef 100644 --- a/textual/src/toga_textual/container.py +++ b/textual/src/toga_textual/container.py @@ -26,7 +26,8 @@ def width(self): @property def height(self): - return self.scale_out_vertical(self.native_parent.size[1]) + # Subtract 1 to remove the height of the header + return self.scale_out_vertical(self.native_parent.size[1] - 1) def refreshed(self): if self.on_refresh: diff --git a/textual/src/toga_textual/widgets/base.py b/textual/src/toga_textual/widgets/base.py index 16879fd834..185332cf1e 100644 --- a/textual/src/toga_textual/widgets/base.py +++ b/textual/src/toga_textual/widgets/base.py @@ -1,5 +1,7 @@ from travertino.size import at_least +from toga.style.pack import ROW + # We assume a terminal is 800x600 pixels, mapping to 80x25 characters. # This results in an uneven scale in the horizontal and vertical directions @@ -64,31 +66,70 @@ def set_tab_index(self, tab_index): # APPLICATOR ###################################################################### - @property - def width_adjustment(self): - return 0 - - @property - def height_adjustment(self): - return 0 - def set_bounds(self, x, y, width, height): - # Convert the width and height back into terminal coordinates, - # subtracting the extra spacing associated with the widget itself. - self.native.styles.width = ( - self.scale_in_horizontal(width) - self.width_adjustment + # Convert the width and height back into terminal coordinates + self.native.styles.width = self.scale_in_horizontal(width) + self.native.styles.height = self.scale_in_vertical(height) + + # Positions are more complicated. The (x,y) provided as an argument is + # in absolute coordinates. The `content_left` ad `content_right` values + # of the layout are relative coordianate. Textual doesn't allow specifying + # either absolute *or* relative - we can only specify margin values within + # a row/column box. This means we need to reverse engineer the margins from + # the computed layout. + parent = self.interface.parent + if parent is None: + # Root object in a layout; Margins are the literal content offsets + margin_top = self.interface.layout.content_top + margin_right = self.interface.layout.content_right + margin_bottom = self.interface.layout.content_bottom + margin_left = self.interface.layout.content_left + else: + # Look for this widget in the children of the parent + index = parent.children.index(self.interface) + if index == 0: + # First child in the container; margins are the literal content offsets + margin_top = self.interface.layout.content_top + margin_right = self.interface.layout.content_right + margin_bottom = self.interface.layout.content_bottom + margin_left = self.interface.layout.content_left + else: + # 2nd+ child in the container. Right and Bottom content offsets are as + # computed by layout. If the parent is a row box, the top offset is as + # computed, but the left offset must be computed relative to the right + # margin of the predecessor. If the parent is a column box, the left + # offset is as computed, but the top offset must be computed relative to + # the bottom margin of the predecessor. + predecessor = parent.children[index - 1] + + margin_top = self.interface.layout.content_top + margin_right = self.interface.layout.content_right + margin_bottom = self.interface.layout.content_bottom + margin_left = self.interface.layout.content_left + + # The layout doesn't have a concept of flow direction; this is a + # property of the style language. However, we don't have any other way + # to establish whether this is a row or a column box. + if parent.style.direction == ROW: + margin_left -= ( + predecessor.layout.content_left + + predecessor.layout.content_width + + predecessor.layout.content_right + ) + else: + margin_top -= ( + predecessor.layout.content_top + + predecessor.layout.content_height + + predecessor.layout.content_bottom + ) + + # Convert back into terminal coordinates, and apply margins to the widget. + self.native.styles.margin = ( + self.scale_in_vertical(margin_top), + self.scale_in_horizontal(margin_right), + self.scale_in_vertical(margin_bottom), + self.scale_in_horizontal(margin_left), ) - self.native.styles.height = ( - self.scale_in_horizontal(height) - self.height_adjustment - ) - - # Apply margins to the widget based on content spacing. - # self.native.styles.margin = ( - # self.scale_in_vertical(self.interface.layout.content_top), - # self.scale_in_horizontal(self.interface.layout.content_right), - # self.scale_in_vertical(self.interface.layout.content_bottom), - # self.scale_in_horizontal(self.interface.layout.content_left), - # ) def set_alignment(self, alignment): pass diff --git a/textual/src/toga_textual/widgets/box.py b/textual/src/toga_textual/widgets/box.py index a3563f9343..01a363ad33 100644 --- a/textual/src/toga_textual/widgets/box.py +++ b/textual/src/toga_textual/widgets/box.py @@ -12,6 +12,9 @@ def create(self): def set_bounds(self, x, y, width, height): super().set_bounds(x, y, width, height) + # The layout doesn't have a concept of flow direction; this is a property of the + # style language. However, we don't have any other way to establish whether this + # is a row or a column box. if self.interface.style.direction == ROW: self.native.styles.layout = "horizontal" else: diff --git a/textual/src/toga_textual/widgets/button.py b/textual/src/toga_textual/widgets/button.py index fff220784a..2ad28bc9a6 100644 --- a/textual/src/toga_textual/widgets/button.py +++ b/textual/src/toga_textual/widgets/button.py @@ -25,14 +25,6 @@ def get_text(self): def set_text(self, text): self.native.label = text - @property - def width_adjustment(self): - return 2 - - @property - def height_adjustment(self): - return 2 - def rehint(self): self.interface.intrinsic.width = at_least(len(self.native.label) + 8) self.interface.intrinsic.height = 3 diff --git a/textual/src/toga_textual/widgets/label.py b/textual/src/toga_textual/widgets/label.py index 2319f40fea..13759ff5e4 100644 --- a/textual/src/toga_textual/widgets/label.py +++ b/textual/src/toga_textual/widgets/label.py @@ -10,7 +10,7 @@ def create(self): self.native = TextualLabel() def get_text(self): - return self.native.renderable + return str(self.native.renderable) def set_text(self, value): self.native.renderable = value From edcab76d9ba861e0a0056aefe2f0f0d8a5d3255b Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 20 Aug 2023 07:30:35 +0930 Subject: [PATCH 10/10] Correct vertical scaling, and make the scaling math explicit. --- textual/src/toga_textual/widgets/base.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/textual/src/toga_textual/widgets/base.py b/textual/src/toga_textual/widgets/base.py index 185332cf1e..da21ee780f 100644 --- a/textual/src/toga_textual/widgets/base.py +++ b/textual/src/toga_textual/widgets/base.py @@ -3,11 +3,12 @@ from toga.style.pack import ROW -# We assume a terminal is 800x600 pixels, mapping to 80x25 characters. -# This results in an uneven scale in the horizontal and vertical directions +# We assume a terminal is 800x600 pixels, mapping to 80x25 characters; +# then deduct 1 row for the titlebar of the window. +# This results in an uneven scale in the horizontal and vertical directions. class Scalable: - HORIZONTAL_SCALE = 10 - VERTICAL_SCALE = 25 + HORIZONTAL_SCALE = 800 // 80 + VERTICAL_SCALE = 600 // 24 def scale_in_horizontal(self, value): return value // self.HORIZONTAL_SCALE