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/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..54174e54b5 --- /dev/null +++ b/docs/reference/platforms/terminal.rst @@ -0,0 +1,46 @@ +======== +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. + +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. 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/examples/tutorial1/tutorial/app.py b/examples/tutorial1/tutorial/app.py index 4cccb0f03a..ffa1854293 100644 --- a/examples/tutorial1/tutorial/app.py +++ b/examples/tutorial1/tutorial/app.py @@ -38,10 +38,10 @@ 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=150, padding_right=10) + join_label.style.update(width=200, padding_right=10) button.style.update(padding=15) 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 77% rename from nursery/textual/MANIFEST.in rename to textual/MANIFEST.in index f477cdd26f..4659263f37 100644 --- a/nursery/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 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..f03f5e35ef --- /dev/null +++ b/textual/src/toga_textual/container.py @@ -0,0 +1,34 @@ +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 + + @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): + return self.scale_out_horizontal(self.native_parent.size[0]) + + @property + def height(self): + # 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: + 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..da21ee780f --- /dev/null +++ b/textual/src/toga_textual/widgets/base.py @@ -0,0 +1,171 @@ +from travertino.size import at_least + +from toga.style.pack import ROW + + +# 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 = 800 // 80 + VERTICAL_SCALE = 600 // 24 + + 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() + + 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): + # 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), + ) + + 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): + 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 new file mode 100644 index 0000000000..01a363ad33 --- /dev/null +++ b/textual/src/toga_textual/widgets/box.py @@ -0,0 +1,25 @@ +from travertino.size import at_least + +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): + 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: + self.native.styles.layout = "vertical" + + 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 new file mode 100644 index 0000000000..2ad28bc9a6 --- /dev/null +++ b/textual/src/toga_textual/widgets/button.py @@ -0,0 +1,30 @@ +from travertino.size import at_least + +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_button_pressed(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.label + + def set_text(self, text): + self.native.label = text + + 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 new file mode 100644 index 0000000000..13759ff5e4 --- /dev/null +++ b/textual/src/toga_textual/widgets/label.py @@ -0,0 +1,20 @@ +from travertino.size import at_least + +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 str(self.native.renderable) + + 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 new file mode 100644 index 0000000000..bedc193704 --- /dev/null +++ b/textual/src/toga_textual/widgets/textinput.py @@ -0,0 +1,65 @@ +from travertino.size import at_least + +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 + + 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_confirm(None) + + +class TextInput(Widget): + def create(self): + self.native = TogaInput(self) + + def get_readonly(self): + return self.native.disabled + + def set_readonly(self, value): + self.native.disabled = value + + 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 + + @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 new file mode 100644 index 0000000000..65ddcc38ae --- /dev/null +++ b/textual/src/toga_textual/window.py @@ -0,0 +1,72 @@ +from textual.screen import Screen as TextualScreen +from textual.widgets import 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()) + + 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.native) + self.set_title(title) + + def create_toolbar(self): + pass + + def clear_content(self): + pass + + def set_content(self, widget): + self.container.content = widget + + self.native.mount(widget.native) + + 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 (self.native.size.width, self.native.size.height) + + 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