From ca9492ac569510ce0a7e5387f81e763a99c7359e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 4 Sep 2022 15:05:44 +0100 Subject: [PATCH 01/24] layout docs --- docs/guide/devtools.md | 17 +++++- docs/guide/layout.md | 56 ++++++++++++++++++ docs/how-to/animation.md | 1 + docs/how-to/mouse-and-keyboard.md | 1 + docs/how-to/scroll.md | 1 + docs/images/layout_align.excalidraw.svg | 16 +++++ docs/images/layout_center.excalidraw.svg | 16 +++++ docs/images/layout_horizontal.excalidraw.svg | 16 +++++ docs/images/layout_table.excalidraw.svg | 16 +++++ docs/images/layout_vertical.excalidraw.svg | 16 +++++ docs/index.md | 2 - mkdocs.yml | 6 +- src/textual/_doc.py | 61 ++++++++++---------- src/textual/message_pump.py | 5 ++ 14 files changed, 194 insertions(+), 36 deletions(-) create mode 100644 docs/how-to/animation.md create mode 100644 docs/how-to/mouse-and-keyboard.md create mode 100644 docs/how-to/scroll.md create mode 100644 docs/images/layout_align.excalidraw.svg create mode 100644 docs/images/layout_center.excalidraw.svg create mode 100644 docs/images/layout_horizontal.excalidraw.svg create mode 100644 docs/images/layout_table.excalidraw.svg create mode 100644 docs/images/layout_vertical.excalidraw.svg diff --git a/docs/guide/devtools.md b/docs/guide/devtools.md index 487700c8e..63f515af0 100644 --- a/docs/guide/devtools.md +++ b/docs/guide/devtools.md @@ -30,13 +30,13 @@ textual run my_app.py:alternative_app When running any terminal application, you can no longer use `print` when debugging (or log to the console). This is because anything you write to standard output would overwrite application content, making it unreadable. Fortunately Textual supplies a debug console of its own which has some super helpful features. -To use the console, open up 2 terminal emulators. In the first one, run the following: +To use the console, open up 2 terminal emulators. Run the following in one of the terminals: ```bash textual console ``` -This should look something like the following: +You should see the Textual devtools welcome message: ```{.textual title="textual console" path="docs/examples/getting_started/console.py", press="_,_"} ``` @@ -47,5 +47,16 @@ In the other console, run your application using `textual run` and the `--dev` s textual run --dev my_app.py ``` -Anything you `print` from your application will be displayed in the console window. You can also call the [`log()`][textual.message_pump.MessagePump.log] method on App and Widget objects for advanced formatting. Try it with `self.log(self.tree)`. +Anything you `print` from your application will be displayed in the console window. + +### Textual log + +In addition to printing strings, Textual console supports more advanced formatting in logs. To write advanced logs import `log` from `textual` as follows: + +```python +from textual import log +``` + +You can logs strings, other Python data types which will be pretty printed in the console. You can also log [Rich renderables](https://rich.readthedocs.io/en/stable/protocol.html). + diff --git a/docs/guide/layout.md b/docs/guide/layout.md index e69de29bb..6f10cf228 100644 --- a/docs/guide/layout.md +++ b/docs/guide/layout.md @@ -0,0 +1,56 @@ +# Layout + +TODO: Explanation of layout + +## Vertical layout + +
+--8<-- "docs/images/layout_vertical.excalidraw.svg" +
+ + +TODO: Explanation of vertical layout + + +## Horizontal layout + +
+--8<-- "docs/images/layout_horizontal.excalidraw.svg" +
+ + +TODO: Explantion of horizontal layout + +## Center layout + +
+--8<-- "docs/images/layout_center.excalidraw.svg" +
+ + +TODO: Explanation of center layout + +## Table layout + + +
+--8<-- "docs/images/layout_table.excalidraw.svg" +
+ + +TODO: Explanation of table layout + + +## Dock + +TODO: Diagram +TODO: Explanation of dock + +## Offsets + +TODO: Diagram +TODO: Offsets + +## Box Model + +TBC diff --git a/docs/how-to/animation.md b/docs/how-to/animation.md new file mode 100644 index 000000000..4709f033e --- /dev/null +++ b/docs/how-to/animation.md @@ -0,0 +1 @@ +# Animation diff --git a/docs/how-to/mouse-and-keyboard.md b/docs/how-to/mouse-and-keyboard.md new file mode 100644 index 000000000..1707580e4 --- /dev/null +++ b/docs/how-to/mouse-and-keyboard.md @@ -0,0 +1 @@ +# Mouse and Keyboard diff --git a/docs/how-to/scroll.md b/docs/how-to/scroll.md new file mode 100644 index 000000000..a12cc92b4 --- /dev/null +++ b/docs/how-to/scroll.md @@ -0,0 +1 @@ +# Scroll diff --git a/docs/images/layout_align.excalidraw.svg b/docs/images/layout_align.excalidraw.svg new file mode 100644 index 000000000..fd2f8e94f --- /dev/null +++ b/docs/images/layout_align.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ZW1PbOFx1MDAxNMff+Vx1MDAxNJn0tXGtozszOzuUy5altEOBXHUwMDAy2d1hXHUwMDE0W0nU+FbbIUCH776yk8FJSCBAmmY3XHUwMDBmmVhcdTAwMTfrSOd3pP9RfmzUavX8JtH1zVpdX3sqMH6qXHUwMDA29bdF+ZVOM1x1MDAxM0e2XG7K5yzup17ZspvnSbb57l2o0p7Ok0B52rkyWV9cdTAwMDVZ3vdN7Hhx+M7kOsx+L74/qVD/lsShn6dONUhD+yaP0+FYOtChjvLMvv0v+1xcq/0ov8esS7WXq6hcdTAwMTPoskNZVVx1MDAxOYhcdTAwMTibLv1cdTAwMTRHpbHAXHUwMDEwYEJcdTAwMTm9b2CyXHUwMDFkO1xcrn1b27Ym66qmKKpnW1x1MDAwM6EvTz5cdTAwMWOdnr3fa1x1MDAxZJxkUVx1MDAwMnvVqG1cdTAwMTNcdTAwMDTH+U1QWuWlcZY1uir3ulWLLE/jnj4zft4tbJsqv++bxXYhql5p3O90I51lXHUwMDEzfeJEeSa/KcpcXPe+dLhcdTAwMTCbtark2j41sHQkRoy4XGaq6Vx1MDAxNn1cdTAwMDGowySarFx1MDAxOJqzXHUwMDFkXHUwMDA31lx01pw3bvmpXGZqKa/XsVZF/n2bPFVRlqjUuqpqN1x1MDAxOE2USOZgLuTEIF1tOt3c1mJcdTAwMTCOIJiPja9LXHUwMDE3MExBuEJWXHUwMDFlLFx1MDAwNk32/Vx1MDAxMoZ/plevq9JktEr1rHhcdTAwMTgzuLB1d4ykqnM/8dXQ41x1MDAxNlx1MDAxNVx1MDAwMClcdEOYVMtcdTAwMWOYqGcro35cdTAwMTBUZbHXqyApS+/evlx1MDAwME6MYVx1MDAxZZyYcNc67Fx1MDAxOXBcdTAwMWX+eXmjm4KdXFxsN1x1MDAwZre/tHZC+s28XHUwMDFjTlghnMJhhJFJXHUwMDA2hnBcdTAwMTKHulj+ZDiJM4dMYFx1MDAwZVx1MDAwMuTKXHUwMDE5bFwiXHUwMDBltKhcdTAwMDOyOjixnafgiMGy4NRBYJJsJpqA5Dw0keQu48DIwmgmX1x1MDAwN9ffZPiRXFxk/T3VXHUwMDEwaciO8jloTuH1y3ZMwFx1MDAwZZZAuUDTOyZGzit5fNNWXHUwMDE0KDxkXHUwMDExgVx1MDAwM2gqXHUwMDEw7mlEyCFcdTAwMTSPmzNiXHUwMDExXHUwMDEzbMstXHUwMDFiq9wnhSDUXHUwMDA1vFxuXHUwMDE0OZ6LXCJcdTAwMDEhXHUwMDAwU1xuXHUwMDBis8j7/n7nNHa1Pr+J6EHe3PrgrzmLyEHUKpVcdTAwMDe7I3ZcdTAwMWSKXHUwMDEwR4K/lsaW69Jl0YiElVx1MDAxOcSVzP1/4oiBz8NRWlx1MDAxMjlnXHUwMDBis9jS+GBcdTAwMDBR0NKn6UHb97RcdTAwMWY1d9abRXskMjnp9+FJbVx1MDAwNSbikr2WRVx1MDAwNC0h2NJYtFwiym7g7D+9NT6e31x1MDAxMHdcdTAwMWWN4Fwiq53AJXhhIHezXXLFaf/w6svOIblIzPlutrXeQDaAO5Iz+iCxYfZcdTAwMDCniD52VHtcdTAwMTJcdTAwMTSol0tHJMlDXHUwMDE4XHUwMDA1OLTQ7kDk8PNcdTAwMDBKZOWmRDbtWT6Uo4qKolx0knJ9Xc1izO2oXHUwMDFm7feDi+/87PiA9D73veOji9v6fbu70a9HkyZhsV9cdPEwllx1MDAxM00nTcz6m3G+MO9KZb3P9HTPoEP5/Xa7Sa9JStebd4RsUsJcXPRcdTAwMDB4zMB5Qlxm2L1VYb1q3u1cdTAwMGVk5YCF/iekSi/jPaTNVFxydHT058fuye5547ZcdTAwMWSe9Z7HOyfAV8Q7XHUwMDEy83hHYNeWXG54XHUwMDA28a7ZYrHMzVx1MDAwMf/jsnnph3nnrIPWm3hcdTAwMGKWQ9hkXHUwMDE2XvS0M3eeuFx1MDAxYii0qGr9XHUwMDAy4IFTzsZud34x8OeXn8yHILyNzuLUfFx1MDAxZPS6unlknlx0vMDAllx1MDAwNfyEnVx1MDAxM1x1MDAxN2JzpbWk3Eo5gVx1MDAxNiZ99qG21qQ3XHUwMDAwZmtcdTAwMTkuXHUwMDFkhtnjm/tcdTAwMDJi5k273Z5cdTAwMDG5mFx1MDAwMTl1p6lGrnCxxf5nbOPLVFx1MDAxNpWH4yg/NrdDKTxRuqdCXHUwMDEz3Ey4qSSyUFx1MDAwNIHpRI2uXHKTW9tQXHUwMDA1m7W/o0C38/F1zbS1onjtmLIu+m9cdTAwMTV9i5d4dl46nVx1MDAwMDw3nlxu7lx1MDAxYoTG98d3e69cdTAwMTjLvjPdX0SGW+M6JlLByVxcm0uLX1x1MDAxNoFzxVWhOFx1MDAwMGOQi4fg7HN2rUNcdTAwMTBcdTAwMDGaI6+4sElmue+/SmC9Klx1MDAwNjHh5Z3zSq9Wni92llx1MDAxZoLTXHUwMDExtYIgfFwiN3g6XGJHXHUwMDA2vCxcZtG8MFx1MDAxNFx1MDAwMPYkJGLxpH724b/WUVxilMyWfJw4jE9dyr9A9L3uILRWMcQpXvH/ks9VYMuPwrRcXJNVXHUwMDA24Vx1MDAxM+nK00E4NHlcdTAwMTiDXHUwMDFiI4VbV0lynNvVtV2GXHUwMDExaVx1MDAxZGj80Vx1MDAxMlUrWr8yevB+Nj1cdTAwMDVAXHUwMDFio7guQkhcdTAwMTfu+3G3cfcvaFx1MDAxZGxTIn0= + + + + align-horizontal: leftalign-horizontal: centeralign-horizontal: right \ No newline at end of file diff --git a/docs/images/layout_center.excalidraw.svg b/docs/images/layout_center.excalidraw.svg new file mode 100644 index 000000000..b9e15e7a9 --- /dev/null +++ b/docs/images/layout_center.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNWXW2/bNlx1MDAxNMff+ylcZve1UXhcdTAwMTdcdTAwMTlgXHUwMDE4nHRZ0mxuV6fpsmFcdTAwMThcdTAwMTiJllx0U5eJtHNDvvsoObUsx1x1MDAwZbzUMDI9XGLQ4eVcdTAwMWPy/Pg/4v2bTqfrblx1MDAwYtU96HTVTSSNjkt53X1X2aeqtDrPfFx1MDAxM6q/bT4po7rnyLnCXHUwMDFl7O+nslx1MDAxYytXXHUwMDE4XHUwMDE5qWCq7URcdTAwMWHrJrHOgyhP97VTqf2xevdlqn4o8jR2ZdA42VOxdnk586WMSlXmrJ/9T//d6dzX74XoSlx1MDAxNTmZJUbVXHUwMDAz6qaFXHUwMDAwXHUwMDAxW7b286xcdTAwMGWWUoxpXGJDMO+g7XvvzqnYt1x1MDAwZX3IqmmpTN2Pn0+MMlx1MDAxZk4tXHUwMDFj9tXPXHUwMDE3VCfclo3XoTZm4G5NXHUwMDFkVVTm1u6NpItGTVx1MDAwZuvKfKy+6tiNfFx1MDAxZrhkn4+1ud+IZlSZT5JRpqxtjclcdTAwMGJcdTAwMTlpd1vZQLOE2UZcdTAwMWN0XHUwMDFhy43/wkBcdTAwMDRIIMF4SOdcctVQRFnAXHUwMDA1oiGHiC6Fc5RcdTAwMWKfXHUwMDA0XHUwMDFmzltQP01AVzJcdTAwMWEnPqosnvdxpcxsIUufqqbf9eNCiWBcdTAwMDFcdTAwMGW5XHUwMDAwbMHJSOlk5KrgXHUwMDEwXHUwMDBmOMFcdTAwMGKRWVWnQISQYoZ5s5bKaXFcdTAwMWHXMPy1vHsjWVx1MDAxNo+71LXVx0LAVaw/LZDUXGaeXHUwMDE0sZxlXHUwMDFjMoaQXHUwMDEwzHuFeN5udDb2jdnEmMaWR+NcdTAwMDaS2vrw7lx1MDAwNXBCLtbBiYDgXHUwMDA0YkrJxnT+dszkP72LL/BzL75y4PBTf7CWziXC2lxcop1yXHUwMDE5glx1MDAxMOBF+r5xXHShgKSFzPa5JMFcdTAwMWEokfePIFx1MDAxMCuwRJwwgVx1MDAwNWW7w1x1MDAxMvtlUlx1MDAxNHK0LSyVMbqwq6Gk4VooXHUwMDExXHUwMDAxXGJ7IdmYSWhcXFx1MDAxNNvxydHFuexHe1NzqT9+eFx0kzvUSlx1MDAwNFx1MDAwMr/h5KlWMuphpeC7tfLtUFJE0VNcdTAwMWUhXG5cdTAwMTAkbS2cXHUwMDEzXHRhQGjroDzyXHUwMDE4+lPCXHUwMDE5RWR3PFwiwSmFLNyaTD7DI4JwXHUwMDFkj1x1MDAxMFx1MDAxMVx1MDAwZVx1MDAxOSB0cyBvpoOz+DzLit4vJ1x0j/hcdTAwMTFOXHUwMDBm81dcdTAwMGUkIUFcdTAwMThS3FaqXHUwMDE5kSSgXHUwMDAyhu26/jJcIq+8wmyLSFx1MDAwMlx1MDAwMWV0p1xuuVNcIlx1MDAxOV1LJIWCXHUwMDAyXHUwMDA0Nq/al45cXJP4MFx1MDAxMX+MP/V+7+9l788y/sqBXGZBgCHHtFVcdTAwMWRnQOJAXGK+JJ4vXHUwMDAxXHUwMDEyoivO2baAhIRcdTAwMDJIOcbof0zks7+SbKEoLzPJIeC+ZjG2MZQ4Oc56N3eEqPIqncqvf5dwcLZcdTAwMDbKkYxGk1K9XHUwMDAyLFx1MDAxOVx1MDAwYsBcbpXEOFxmQkp5XHUwMDFi15fXbbZcdTAwMDJL7zb45qKeiK2gk/tb0PLReOSTcsaxXHUwMDFmvlPF9GXD/83RbfHp1I1bhWbI15HJWMh8XHKHm5dvfX7X+7KX0PjXy/MzejhMU927fe1g+ppcdTAwMTCQZakkSFx1MDAwNP5g0u+WymdvOIw85XCFPFwi4C+9XHUwMDAy7PSi/d/VsUlznrmBvpvdk1vWY5lqc9vKVI2lj9TnPVFucSut8j5nXG7Z6t0zOqnA7Vx1MDAxYTVsXHUwMDEz7XQkzbzZ5Vx1MDAwYiuPvHfppytP4+VV5KVOdCbNeTuS2WHy73qPurIoXHUwMDA2zu+Q7zE7Wj5cdDp+XFxmM193qtX14Spxqp9q1vqAVmdBVSm4f3jz8C/DXFz9NyJ9 + + + + Widget \ No newline at end of file diff --git a/docs/images/layout_horizontal.excalidraw.svg b/docs/images/layout_horizontal.excalidraw.svg new file mode 100644 index 000000000..25ce496f2 --- /dev/null +++ b/docs/images/layout_horizontal.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2aa0/bSFx1MDAxNIa/8ytQ+rVM536ptFpcdTAwMTEuLTQtlNAtdFVVjj1JZnFsYztcdGnFf9+xQ+PE2GxcYlHKatdCSTzX45nnXHUwMDFjvzPDj63t7UY6iXTj9XZD37iOb7zYXHUwMDE5N15m6SNcdTAwMWQnJlxmbFx1MDAxNs7vk3BcdTAwMTi7ecl+mkbJ61evXHUwMDA2Tnyl08h3XFxcckYmXHUwMDE5On6SXHUwMDBlPVx1MDAxM1x1MDAwMjdcdTAwMWO8MqlcdTAwMWUkv2efXHUwMDFmnIH+LVxuXHUwMDA3Xlx1MDAxYYOik1x1MDAxZO2ZNIynfWlfXHUwMDBmdJAmtvU/7f329o/8c866WLupXHUwMDEz9HydV8izXG5cdTAwMDNcdTAwMTFH5dRcdTAwMGZhkFx1MDAxYitcdTAwMDRnTFBOZ1x1MDAwNUyyb7tLtWdzu9ZkXeRkSY1wrHY6o1x1MDAxMbuiTv8vfja8uWw3o6LXrvH9djrxc6vcOEySnb6Tuv2iRJLG4ZX+bLy0n9lWSp/VTUI7XHUwMDEwRa04XHUwMDFj9vqBTpKFOmHkuCadZGlcdTAwMTDOUqdcdTAwMDPxertIubF3XHUwMDFjXHUwMDAxXHUwMDA0XHUwMDEx45jNkvOKXHUwMDA0XHUwMDAyXHUwMDBlXHUwMDA1xUhcblYyZi/07Vx1MDAxNFhjXsD8KszpOO5Vz9pcdTAwMTR4szJp7Fx1MDAwNEnkxHaiinLju8ekilx1MDAwM1wipILz3fe16fVTm0uwXHUwMDA0kpL5/nU+XHUwMDAxXGJhXHUwMDAyle2ZzHKyXqMjL2fha3nw+k5cdTAwMWPdXHJSI8lu5izOjD2YXHUwMDAzqag8jDxnOuGIc4yVXCJcbjNajJ5vgiubXHUwMDE5XGZ9v0hcdTAwMGLdq4KRPPX25SpsXHUwMDEyXsemopxcdTAwMGKO4fJs9odB29//LryTZvzHXHUwMDA1jpLjT+ZdXHKbJb5cdTAwMTapxJukktPFuc8rYlx1MDAwNVx1MDAwNJSqRMXaqaSgXHUwMDA2ScxcdTAwMDHCXGKqKii55Vx1MDAxMVNcdTAwMDHR5qAkXHUwMDEwYiigQOuCUvu+iZJqJFx1MDAxMalDkiNCqP1cdTAwMTNLI/lpsPd2ctChqOd8eXt4zVvHcFx1MDAxZq2C5OZcdTAwMDKlwFx1MDAwMImFaDiNk1xuMIZcdTAwMDV5KpEvulx1MDAwZcNcZt+nXHUwMDExYYBRyVx1MDAxN2Y8XCJcdTAwMDQoI1xi36NcdTAwMTFbw1xiUYJvNERaKyGkXHUwMDFioZGLOlx1MDAxYe1gXHREkIBqaVx1MDAxY9E5vWmpTtg62lx1MDAxOUYnh63v3eB493njaN+cwlx1MDAwZYLi94mUgFx1MDAxMlXGYiVcIjtcdTAwMTCytVx1MDAxMVx0MWFIWiY3TyTeXHUwMDAwkVx1MDAxONfKSeuLUmJCKVmayPeuuL5pXlx1MDAxY6TiYHzcla0v+/1o/3lcdTAwMTOJsOWC2ZBTISaFXHUwMDE1ckzAJyOJcEdKvi4kXHUwMDExQlx1MDAxMCumXHUwMDE4+1x1MDAxNyP5oI7k9WtcdTAwMWOroZVgXG7hpZmM3r9PzpvDvdZxcn74RSr+5qzztobJvuP2h7H+9VRcblx1MDAwMaxywVx1MDAxMpWZXHUwMDE0XGYwWKZ19Vx1MDAxNzevopJZuShcdFOVWGIuXHUwMDAxrMKSWIVPIedqk1QqSlx1MDAxMCNiXVSm+iatVpGyXHUwMDE2SIVskGRKLS8jj1x1MDAwZttJ2mpCdlx1MDAxMnjJ8FrH74KLi+dO5DROksVcdTAwMTVGVlx1MDAxNUtcYlxixvjJSD64upnb1ChQrFjNWFxirYhcIlx1MDAxYlxczWShUVwiwsgjICzmOlxm0rb5rnOlsZB66FxmjD9ZmK6cTmupnfyeTufHMtG2z5xGuVB61ze9jN+Gr7uLYKfGdfxZdlx1MDAxYc49uWt7d2xz8ZFXfoowNj1cdTAwMTM4/vmiJatHeilonWNcdFx1MDAxYuelJIIv7Ve4NWiN+67eXHUwMDFm6dbHyWRMh2ejo+fuV5gxUN5cdTAwMWGYRnr7XG6AVns+OdJP1UdlpIdcdTAwMWPYt7pYeM3MRXpcdTAwMDZ4aZPtp59Jqz6gIHijXHUwMDEyRFFcdTAwMWJq4WP8bHUwXHUwMDE1YnVgXCJMoWBYLs3ldde9hEcn15+Cvve5N1x1MDAxYZs2Pfz23Lkkklx1MDAwMJS90O8pXHUwMDEwXHUwMDA0eFmarIIlxrKjq7HkXHUwMDE4SJ53QVV2iSo4XHUwMDE1kFx1MDAwNFcqXHUwMDExpCRWVJBccitcdTAwMTEsXHUwMDA15OuCs1aJqHouKYNcdTAwMWOJR+xnnYs3x6NT7/SjY051e6/3wfF26/aznlxymFhIIGBpVTZcdTAwMTVcIlx1MDAxMqh1bCGsQYhQqlxis4vHTetcdTAwMTAqXHUwMDFmJYb/Szqk1qPkXHUwMDAzXHUwMDEyhNs4w+dm8Vx1MDAxZlx1MDAwZtQ+XHUwMDFmOJff6NklvDa9sVaBOPlr8tw9imJcdTAwMDHKbvPToaDgpf3jX6TsieJ2sUfxpj0qO7T636Oyr3tcdTAwMWXlxHE4rnQpWOtSdrVoXHUwMDAzOH/EnuK3uHnK946uXHUwMDA35uDj8I1vlHmz667mUlx1MDAxYjxcdJSA4vu73FxmXCLwkCtJ0WWdp1x1MDAxY1x1MDAwMTJA+KK7XHUwMDE2+4mAK1U6XHUwMDE4v/MtaHOs4EUr7HHn1q3mW1xmMkVcdTAwMWVz6jJnh1x1MDAxM6dNXHUwMDEzeCbolavowKvJ8Z0k3Vx1MDAwYlx1MDAwN1x1MDAwM5NaM05DXHUwMDEzpOVcdTAwMTJ5u7tcdTAwMTnVfe3cc1x1MDAxMdvyfF5cdTAwMTn/KGux+K+O7Cp+bVx1MDAxN3zkN7PfX19Wlq6Yyewq5rBoYGv++3brrsmGXHUwMDEzRe3UXHUwMDBluDVo6rh2To13XHUwMDE3kYrnaoyMXHUwMDFlN6v2XHUwMDA38ytcdTAwMGJcdTAwMDC5+2d+prOn+3G7dfs38GbaXHUwMDA3In0= + + + + WidgetWidgetWidget \ No newline at end of file diff --git a/docs/images/layout_table.excalidraw.svg b/docs/images/layout_table.excalidraw.svg new file mode 100644 index 000000000..b1c03573d --- /dev/null +++ b/docs/images/layout_table.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ZW0/bWFx1MDAxMMff+ylQ+tq4Z+bcK61WNFxcWiCUkt1cdTAwMDK7WlWO41x1MDAxMINcdTAwMTNcdTAwMWLbIUDV775jhyU33KZpakXrXHUwMDA3Kz5zLmP7l5n/XHUwMDFjf3mxtVXL7mO/9mar5t95blx1MDAxOHRcdTAwMTJ3VHuVt9/6SVx1MDAxYURcdTAwMDMyYXGdRsPEK3r2sixO37x+3XeTaz+LQ9fzndsgXHUwMDFkumGaXHI7QeR4Uf91kPn99Pf8fOz2/d/iqN/JXHUwMDEyZ7JI3e9cdTAwMDRZlIzX8kO/71x1MDAwZrKUZv+brre2vlx1MDAxNOcp71x1MDAxMt/L3MFl6Fx1MDAxN1x1MDAwM1xu05SDTM23XHUwMDFlR4PCWVx1MDAwNOTaWG6fOlx1MDAwNOlcdTAwMGUtl/lcdTAwMWSydsllf2LJm2pcdTAwMTdcdTAwMDfN7bOP7frZyd1d4+P1kN1cdTAwMWPeXHUwMDFkTFbtXHUwMDA2YdjK7sPCKy+J0rTeczOvN+mRZkl07Z9cdTAwMDWdrEd9YK79aWxcdTAwMWHRg5iMSqLhZW/gp+nMmCh2vSC7z9tcdTAwMTh7alx1MDAxZD+IN1uTlruih3CEZchcdTAwMDXX8slSjFXGYahnXHJjd1x1MDAxYVFIL4HcecmKY+JQ2/WuL8mrQeepT5a4gzR2XHUwMDEzelWTfqPHXHUwMDFiXHUwMDE1Vjn5o2ZcbieL9PzgspeRlaNxzOz6fvFcblx1MDAwMFx1MDAxOFdWXHUwMDAzm1jyVeP3nYKGf+ZcdTAwMWZfz03ix8dUS/OLKY9zZ3enUJpcZlx1MDAxZcZcdTAwMWR3/MpBKURrpURE82RcdTAwMGaDwTVcdTAwMTlcdTAwMDfDMJy0Rd71hJKi9eurXHUwMDE16Fx1MDAwNFx1MDAwM2V0guBKaCNcdTAwMDCWxvNcYsxprD9fdi5cdTAwMWV6O63zXHUwMDA2unB+VYLnXHUwMDFjYrNgYqVgcqlcdTAwMDWCWVx1MDAwMFM7XHUwMDAytFXml4IpnFx1MDAxMipROYDA7HNcXGpjXHUwMDE0SKOhOi45Y2BcYk25Li79MFxm4vR5KqUujZkoKIqgxaWhvOrsXHUwMDFknkrVPd/fPYhakDWu9o6OVoGywmhcdMIxhlx1MDAxOWlcdTAwMTeipVaO1ajkNDIrQPmy60qUuFxiJKCDIGaj4Vx1MDAxM5JcdTAwMDCOkFx1MDAxY3BcdTAwMDFII1x1MDAwNCCNstXxiNYoxrTBXG54RCiPknTboJiQy1x1MDAwM3kxVKOjpOPtn7jhcFx1MDAwZmL3Pey3N1x1MDAxY0huXHUwMDFkgVKb6Xc/5lE6c5iuRmObMbkuXHUwMDFhuZVGXHUwMDE4i/p/SqOSpTRKsJIhXHUwMDEzS8PoS+1dnN6c3lxmP0Rvd0+McuuNvVxyh1FRzlRcdTAwMGLpmkhcXENcXFx1MDAwNGxTbl1cdTAwMTeJkKdpbqyqMFGvXHUwMDFkxW9cbkhrTVx1MDAxOYzSkuLnQsilYey6Ot1vXFy+P/N6fHR80N5pZp/LUnXP9XrDxN9cdTAwMDBcdTAwMWOBqlx1MDAwN2VBzMi4fCwyiprCzkrL1ZO1elx1MDAwNktkzGGP6rWYiC/SqfE/XHUwMDFkK2xxLJY5hkojToK30jJH0YpcYpVQXG6kmMsw5VxmhEa5fFx1MDAxMZ707el+eLB90k/f3orgoKWPd+NNp5RzokAzzfh8XG7PKdWG46xlvZhcdTAwMWHm6HGJP55cdTAwMDdWoVx1MDAxNCWn0GZs1ZRcbjS6XCJKVWndQ9U4ZVx1MDAxMSHN8tX4X432fnB1XHUwMDA28Knree+uXHUwMDFlLpvHO+6mcyqQ6nEln9koQkrBQuHcJs56OaVk7fBcXM4+MjjF2vKg0lx1MDAxZkqRXGJcdTAwMDNd7a6R4sZIUVxyqGxKXcyHU7p3kNOJ6HucNk+UVz+Fm/1MRkN5eNjubsNg0zlcdTAwMDXMNZ9kiyVcdTAwMTFn6Fx1MDAwMJvfUVpdi/66tE/AUEZQrFJxaiVJRlVR2kdRXrhcdTAwMDMgKVx1MDAxMMaXl6cgdlx1MDAwMqyLXHUwMDExXHUwMDBm7Fx1MDAwNV5cdTAwMWaMwtG7/t2mg8q5cJixZiGeckZllDVM/OxcdTAwMDbnNzhdT95XUkqOouK8XHUwMDBmjH5UgymXvFx1MDAxNFOhlFGag15cdTAwMWFTb7dxdXHYPKvzT6zVvv/ktnpRuOmYXG60jmTymS1Pzlx1MDAxNFnAyp/Wp+WcgrCOnT5WyftcdTAwMDCSdFx1MDAwYlx1MDAxM9XugloltGZr25X/TjyV5Vx1MDAxZjNcdTAwMTknSjmI5Vx1MDAwNWrrfK/ZxEHWYSzTf/x5XFw/yuKdTVx1MDAwNzVP/FZcdTAwMGLFXHUwMDE2QbXaoeQ2Z1lcdTAwMDVURNP2n0/8XHUwMDE2SFx1MDAxZetcdTAwMTlp8SOhNP9yXHUwMDA0lPaqJVx1MDAxNOn/W9H3TNI0paFcdTAwMTSlUcL+XGKhn/vx1Yc06D58XGa3e7fRUe9cdTAwMTQu2KZcdTAwMTOah1LEMkKZYXOidb2EUsJy7PhTgfiJXHUwMDEySoE1WHkopVxmJNRcdTAwMGaDSudi0ppcdTAwMWLHrYymJPNcdTAwMThb8jrotIJcdTAwMDd/ZprabeCP3j5XmFx1MDAxNkftxSP8OV9+7vOXry++/lx1MDAwYntetL4ifQ== + + + + \ No newline at end of file diff --git a/docs/images/layout_vertical.excalidraw.svg b/docs/images/layout_vertical.excalidraw.svg new file mode 100644 index 000000000..fff71ecf9 --- /dev/null +++ b/docs/images/layout_vertical.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2ZW2/aSFx1MDAxNIDf8ytcIvrauHO/VFqtXHUwMDAyTdrck9KkTVdVNLFcdTAwMDdwMLbXXHUwMDFlXHUwMDEyoOp/37FJMVx1MDAxOFx1MDAxY1FcdTAwMWFRtrt+MPjM7XjmO2fOXHUwMDE5f93a3q6ZYaxrr7dreuCqwPdcdTAwMTL1UHuZye91kvpRaItQ/pxG/cTNa3aMidPXr171VNLVJlx1MDAwZZSrnXs/7asgNX3Pj1x1MDAxYzfqvfKN7qV/ZvdT1dN/xFHPM4lTXGayoz3fRMl4LFx1MDAxZOieXHUwMDBlTWp7/8s+b29/ze9T2iXaNSpsXHUwMDA3Om+QXHUwMDE3XHUwMDE1XG5CLsrS0yjMlYVcdTAwMDJRxFx1MDAxMGZsUsNP39jxjPZsccvqrIuSTFS7rNdcdTAwMGbj+/6RSNBdfFF/84lcdTAwMGZIUlxm2/KDoGmGQa6Wm0RputNRxu1cdTAwMTQ1UpNEXf3R90wn06Akn7RNIztcdTAwMTNFqyTqtzuhTtOZNlGsXFzfXGYzXHUwMDE5XHUwMDAwXHUwMDEz6XgmXm9cdTAwMTeSQbZOQjqSUFxmXHUwMDExncjzloI6XHUwMDE4gVx1MDAxOflYl0ZcdTAwMTTYJbC6vFx1MDAwMPlVaHOr3G7bqlx1MDAxNHqTOiZRYVx1MDAxYavELlRR7+HxLYlkXHUwMDBl5kJcdTAwMDI2NUhH++2OsaVcdTAwMThcdEdcdTAwMTDMp8bX+fxD24ZcdTAwMGLBWVGSjVx1MDAxYVx1MDAxZng5XHUwMDBiX8pz11FJ/DhHtTR7mNI4U3ZvXG6konE/9tR4vSFjXGJJiVx1MDAwMVx1MDAxN6yYvMBcdTAwMGa7tjDsXHUwMDA3QSGL3G6BSC799nJcdTAwMTU2KapiU2AkhSRkeTRcdTAwMTk4XCJcdTAwMThGh+qm8/Gi07jY8y/gTVx1MDAwNZolvGahROuDUlx1MDAwModIXHUwMDA0XHUwMDA1L0NJXHUwMDFjgGd5eX4oiVNBJGJcdTAwMGVEXHUwMDEwyFx1MDAwNUxcIkgpwFx1MDAxOK5cdTAwMTFJXGZcdTAwMDCUjHD0XFxI6iDw43QxkKjSWVxujJl1XHUwMDE0kixccuS+PLl517x6f/k+uvp09Fx1MDAwZXXdYd1bXHUwMDA1yPV5SVxmoFx1MDAwMyBlZSdpWSmJV8DxRUtRu+HMo1xikYMgmfWBXHUwMDEzXHUwMDE4IXRKbvu7e+SIQYq5/Fx1MDAxN3vHp1BcdTAwMTSVvpFbs4VcdTAwMThRsDSKny/SgTm6eXu81/Aurlx1MDAxM91NSXC04ShcIupQXHUwMDA2KGFz3lFcIuu5XHUwMDEwnt0zV+LxXHUwMDE2XHUwMDAw+lxcPFwiwFx0XHUwMDAzkEn8e1x1MDAwMmnjxCogXHQj2DpqKZZcdTAwMDbyslx1MDAxZTY/XHUwMDFm3lx1MDAwNVx1MDAwZnz0rn/gN+7vXFx8sOFAUuhAYO9cdTAwMGLcI3IwXHUwMDEwVP4skFx1MDAxMN1cbsGeXHUwMDBiSMKJpIKJ3zZ8xE+kNvaShEO8PJLnXHUwMDFmklH3nDY8vvu3fHOTXHUwMDA0QI3OKpDsKLfTT/RcdTAwMDZAXHSBXHUwMDAz5YJcdTAwMTDSukeHlZBZfc+mXHUwMDBivCQhwsk8nlxcSCW1Sc04r1wiMrtcdTAwMDQr44lcdTAwMTBEmGNcdTAwMDLXiifN7Eg8XHUwMDE3nkZcdTAwMGbMQl9Z6SohQ4xhgsDyiVxybV2NTsO3g+udYDS63mVR86ZcdTAwMTFvOpg2S5jlkZKf4fDJVIaRef5cdTAwMTbEi1x1MDAxNkKbSeBcco9cdTAwMTeLdY1C0/RHOlx1MDAwZi1mpPuq51x1MDAwN8OZpclBtJrahW5rMz2VqbZjjk97ZmrvXHUwMDA2fjtDtVx1MDAxNujWLMPGd1UwKTbR1Ju7dnRlu0tcdTAwMGW88ltEid/2Q1x1MDAxNXyY1WR1705cdTAwMTCv9u6SXHUwMDAwIaRcXD5cdTAwMDKW51x1MDAxMFx1MDAxY1x1MDAwZY7htZa3rZM9fkz76d6mXHUwMDFiXHUwMDExXHUwMDA20mFcdTAwMDLPXHUwMDA2XHUwMDE2w9ztXHUwMDEzR1xiJH7y1OpcdTAwMDVcdTAwMDEuoJwtXGI5XGIlXHUwMDBlp7jihFx1MDAwMELqXGLISFaaXHUwMDBmXHUwMDAz56yNXCJJKON0vdFcdTAwMDdcdTAwMDXUboZriT4oJlV8YmRcdTAwMTM0xvnyeKrPO83hzd6nncuTw+O6XHUwMDFj+Gr/Q3Pj8bTBXHUwMDA3gVxczmVoWWhApSzFXHUwMDA2K1x1MDAwMeoy3aKLXHUwMDAxtUFxJaCEOyjXazzIPJ9cYoBcZm+I15utUWhDtWfjUyVJ9LD4XHUwMDFjqzpX43ZcdTAwMGKU8Fx1MDAwN+KP+5OUjc5cdTAwMGUur0x8XHUwMDE2JOfDs1x1MDAwN4YvVmNzfUerwsa/slx1MDAxY1x1MDAwMH8/yypHrWUybVx1MDAxYaawfprMqlxcXHI4nHO8OFXDXGI5XHUwMDA0US5cdTAwMTZcdTAwMWZnXHRcblxiWyFcdTAwMWPONVt3eJJcdTAwMWGVmLpcdTAwMWZ6ftguN9GhV1FcdTAwMTKo1DSiXs83Vo3zyFx1MDAwZk25Rt7vblx1MDAwNnZHq7kow/Y8XVa2gDjrsfhSll3Fv+1cdTAwMDKR/GHy/8vLxbXnVjK7ptew6GFr+vdHs1x1MDAwNVwiy8JJoGMxlVhQtLy1plx1MDAwN/qwvdc+xfuXcrA/knetNiSbvpNYN+0ghNjc0Vxutnnk/LeIX5M/XHUwMDAwTG3ehjb9POW/lEBUWZR84ps3QcKGrj/wzftYJo2O2b3bXHUwMDFk6uOr3TPVc+/ev918i+JcdTAwMGVcdTAwMTFg/vScWIviXGbymVx1MDAxM6NfZFFYXHUwMDEwXHS53aP/t6i1W9TW475XU3HcNHaGbI2xfdlF8L3H1yz6q937+qG+6IQwv7JecyvN7EFnS/D129a3f1x1MDAwMLFE1Vx1MDAwMCJ9 + + + + WidgetWidgetWidget \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index a3fdfc8ea..dd59b082b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -70,7 +70,5 @@ Textual is a framework for building applications that run within your terminal. === "Example 2" - ```{.textual path="docs/examples/introduction/timers.py"} - ``` diff --git a/mkdocs.yml b/mkdocs.yml index 1e84692a9..46367e352 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,9 +7,13 @@ nav: - "introduction.md" - Guide: - "guide/devtools.md" + - "guide/layout.md" - "guide/CSS.md" - "guide/events.md" - + - How to: + - "how-to/animation.md" + - "how-to/mouse-and-keyboard.md" + - "how-to/scroll.md" - "actions.md" - Events: - "events/blur.md" diff --git a/src/textual/_doc.py b/src/textual/_doc.py index 88770bd95..6a73696a6 100644 --- a/src/textual/_doc.py +++ b/src/textual/_doc.py @@ -1,5 +1,6 @@ from __future__ import annotations +import runpy import os from typing import cast, TYPE_CHECKING @@ -11,36 +12,36 @@ if TYPE_CHECKING: def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str: """A superfences formatter to insert a SVG screenshot.""" - path: str = attrs["path"] - _press = attrs.get("press", None) - press = [*_press.split(",")] if _press else ["_"] - title = attrs.get("title") - - os.environ["COLUMNS"] = attrs.get("columns", "80") - os.environ["LINES"] = attrs.get("lines", "24") - - print(f"screenshotting {path!r}") - - cwd = os.getcwd() - examples_path, filename = os.path.split(path) try: - os.chdir(examples_path) - with open(filename, "rt") as python_code: - source = python_code.read() - app_vars: dict[str, object] = {} - exec(source, app_vars) + path: str = attrs["path"] + _press = attrs.get("press", None) + press = [*_press.split(",")] if _press else ["_"] + title = attrs.get("title") - app: App = cast("App", app_vars["app"]) - app.run( - quit_after=5, - press=press or ["ctrl+c"], - headless=True, - screenshot=True, - screenshot_title=title, - ) - svg = app._screenshot - finally: - os.chdir(cwd) + os.environ["COLUMNS"] = attrs.get("columns", "80") + os.environ["LINES"] = attrs.get("lines", "24") - assert svg is not None - return svg + print(f"screenshotting {path!r}") + + cwd = os.getcwd() + try: + app_vars = runpy.run_path(path) + app: App = cast("App", app_vars["app"]) + app.run( + quit_after=5, + press=press or ["ctrl+c"], + headless=True, + screenshot=True, + screenshot_title=title, + ) + svg = app._screenshot + finally: + os.chdir(cwd) + + assert svg is not None + return svg + + except Exception as error: + import traceback + + traceback.print_exception(error) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 4700538d8..01cecabca 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -112,6 +112,11 @@ class MessagePump(metaclass=MessagePumpMeta): @property def log(self) -> Logger: + """Get a logger for this object. + + Returns: + Logger: A logger. + """ return self.app._logger def _attach(self, parent: MessagePump) -> None: From 432db215126d49e04eef4a42c2a68c02806c6d6c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 4 Sep 2022 20:40:12 +0100 Subject: [PATCH 02/24] app basics and doc structure --- docs/examples/app/event01.py | 30 ++++++++ docs/examples/app/return.py | 14 ++++ docs/examples/app/simple01.py | 5 ++ docs/examples/app/simple02.py | 10 +++ docs/examples/app/widgets01.py | 15 ++++ docs/guide/app.md | 94 +++++++++++++++++++++++++ docs/guide/devtools.md | 64 +++++++++++++++-- docs/images/layout_table.excalidraw.svg | 6 +- docs/reference/button.md | 1 + docs/widgets/button.md | 1 + docs/widgets/data_table.md | 1 + docs/widgets/footer.md | 1 + docs/widgets/header.md | 1 + docs/widgets/static.md | 1 + docs/widgets/tabs.md | 1 - docs/widgets/tree_control.md | 1 + mkdocs.yml | 11 ++- src/textual/__init__.py | 5 ++ src/textual/_log.py | 1 + src/textual/app.py | 35 ++++----- src/textual/widgets/__init__.py | 1 + src/textual/widgets/__init__.pyi | 1 + src/textual/widgets/_button.py | 3 +- src/textual/widgets/_welcome.py | 58 +++++++++++++++ 24 files changed, 331 insertions(+), 30 deletions(-) create mode 100644 docs/examples/app/event01.py create mode 100644 docs/examples/app/return.py create mode 100644 docs/examples/app/simple01.py create mode 100644 docs/examples/app/simple02.py create mode 100644 docs/examples/app/widgets01.py create mode 100644 docs/guide/app.md create mode 100644 docs/reference/button.md create mode 100644 docs/widgets/button.md create mode 100644 docs/widgets/data_table.md create mode 100644 docs/widgets/footer.md create mode 100644 docs/widgets/header.md create mode 100644 docs/widgets/static.md delete mode 100644 docs/widgets/tabs.md create mode 100644 docs/widgets/tree_control.md create mode 100644 src/textual/widgets/_welcome.py diff --git a/docs/examples/app/event01.py b/docs/examples/app/event01.py new file mode 100644 index 000000000..a19a19d3c --- /dev/null +++ b/docs/examples/app/event01.py @@ -0,0 +1,30 @@ +from textual.app import App +from textual import events + + +class EventApp(App): + + COLORS = [ + "white", + "maroon", + "red", + "purple", + "fuchsia", + "olive", + "yellow", + "navy", + "teal", + "aqua", + ] + + def on_mount(self) -> None: + self.styles.background = "darkblue" + + def on_key(self, event: events.Key) -> None: + if event.key.isdecimal(): + self.styles.background = self.COLORS[int(event.key)] + + +app = EventApp() +if __name__ == "__main__": + app.run() diff --git a/docs/examples/app/return.py b/docs/examples/app/return.py new file mode 100644 index 000000000..9e37b4276 --- /dev/null +++ b/docs/examples/app/return.py @@ -0,0 +1,14 @@ +from textual.app import App, ComposeResult +from textual.widgets import Button + + +class ButtonsApp(App): + def compose(self) -> ComposeResult: + yield Button("Paul") + yield Button("Duncan") + yield Button("Chani") + + +app = ButtonsApp() +if __name__ == "__main__": + app.run() diff --git a/docs/examples/app/simple01.py b/docs/examples/app/simple01.py new file mode 100644 index 000000000..03a13218e --- /dev/null +++ b/docs/examples/app/simple01.py @@ -0,0 +1,5 @@ +from textual.app import App + + +class MyApp(App): + pass diff --git a/docs/examples/app/simple02.py b/docs/examples/app/simple02.py new file mode 100644 index 000000000..8b4fec9ce --- /dev/null +++ b/docs/examples/app/simple02.py @@ -0,0 +1,10 @@ +from textual.app import App + + +class MyApp(App): + pass + + +app = MyApp() +if __name__ == "__main__": + app.run() diff --git a/docs/examples/app/widgets01.py b/docs/examples/app/widgets01.py new file mode 100644 index 000000000..baeb3bec8 --- /dev/null +++ b/docs/examples/app/widgets01.py @@ -0,0 +1,15 @@ +from textual.app import App, ComposeResult +from textual.widgets import Welcome + + +class WelcomeApp(App): + def compose(self) -> ComposeResult: + yield Welcome() + + def on_button_pressed(self) -> None: + self.exit() + + +app = WelcomeApp() +if __name__ == "__main__": + app.run() diff --git a/docs/guide/app.md b/docs/guide/app.md new file mode 100644 index 000000000..f578b5a5c --- /dev/null +++ b/docs/guide/app.md @@ -0,0 +1,94 @@ +# App Basics + +In this chapter we will cover what you will need to know to build a Textual application. Just enough to get you up to speed. We will go in to more detail in the following chapters. + +## The App class + +The first step in building a Textual app is to import the [App][textual.app.App] class and create a subclass. Let's look at the simplest app class: + +```python +--8<-- "docs/examples/app/simple01.py" +``` + +### The run method + +To run an app we create an instance and call [run()][textual.app.App.run]. + +```python hl_lines="8-10" title="simple02.py" +--8<-- "docs/examples/app/simple02.py" +``` + +Apps don't get much simpler than this—don't expect it to do much. + +!!! tip + + The `__name__ == "__main__":` condition is true only if you run the file with `python` command. This allows us to import `app` without running the app immediately. It also allows the [devtools run](devtools.md#run) command to run the app in development mode. See the [Python docs](https://docs.python.org/3/library/__main__.html#idiomatic-usage) for more information. + +If we run this app with `python simple02.py` you will see a blank terminal, something like the following: + +```{.textual path="docs/examples/app/simple02.py" title="simple02.py"} +``` + +When you call [App.run()][textual.app.App.run] Textual puts the terminal in to a special state called *application mode*. When in application mode, the terminal will no longer echo what you type. Textual will take over responding to user input (keyboard and mouse) and will update the visible portion of the terminal (i.e. the *screen*). + +If you hit ++ctrl+c++ Textual will exit application mode and return you to the command prompt. Any content you had in the terminal prior to application mode will be restored. + +## Events + +Textual has an event system you can use to respond to key presses, mouse actions, and also internal state changes. Event handlers are methods which are prefixed with `on_` followed by the name of the event. + +One such event is the *mount* event which is sent to an application after it enters application mode. You can respond to this event by defining a method called `on_mount`. + +Another such event is `on_key` which is sent when the user presses a key. The following example contains handlers for both those events: + +```python title="event01.py" +--8<-- "docs/examples/app/event01.py" +``` + +The `on_mount` handler sets the `self.styles.background` attribute to `"darkblue"` which (as you can probably guess) turns the background blue. Since the mount event is sent immediately after entering application mode, you will see a blue screen when you run the code: + +```{.textual path="docs/examples/app/event01.py" hl_lines="23-25"} +``` + +The key event handler (`on_key`) specifies an `event` parameter which should be a [events.Key][textual.events.Key] instance. Every event has an associated event object which will be passed to the handler method if it is present in the method's parameter list. + +!!! note + + It is unusual for a method's parameters to affect how it is called. Textual accomplishes this by inspecting the method prior to calling it. + +For some events, such as the key event, there is additional information on the event object. In the case of [events.Key][textual.events.Key] it will contain the key that was pressed. + +The `on_key` method above uses the `key` attribute on the Key event to change the background color if any of the keys 0-9 are pressed. + +### Async events + +Textual is powered by Python's [asyncio](https://docs.python.org/3/library/asyncio.html) framework which uses the `async` and `await` keywords to coordinate events. + +Textual knows to *await* your event handlers if they are generators (i.e. prefixed with the `async` keywords). + +!!! note + + Don't worry if you aren't familiar with the async programming in Python. You can build many apps without using them. + +## Widgets + +Widgets are self-contained components responsible for generating the output for a portion of the screen and can respond to events in much the same way as the App. Most apps that do anything interesting will contain at least one (and probably many) widgets which together form a User Interface. + +Widgets can be as simple as a piece of text, a button, or a fully-fledge component like a text editor or file browser (which may contain widgets of their own). + +### Composing + +To add widgets to your app implement a [`compose()`][textual.app.App.compose] method which should return a iterable of Widget instances. A list would work, but it is convenient to yield widgets, making the method a *generator*. + +The following example imports a builtin Welcome widget and yields it from compose. + +```python title="widgets01.py" +--8<-- "docs/examples/app/widgets01.py" +``` + +When you run this code, Textual will *mount* the Welcome widget which contains a Markdown content area and a button: + +```{.textual path="docs/examples/app/widgets01.py"} +``` + +Notice the `on_button_pressed` method which handles the [Button.Pressed][textual.widgets.Button] event send by the button contained in the Welcome widget. The handlers calls [App.exit()][textual.app.App] to exit the app. diff --git a/docs/guide/devtools.md b/docs/guide/devtools.md index 63f515af0..a8ed972c1 100644 --- a/docs/guide/devtools.md +++ b/docs/guide/devtools.md @@ -1,5 +1,13 @@ # Devtools +!!! note inline end + + If you don't have the `textual` command on your path, you may have forgotten so install with the `dev` switch. + + See [getting started](../getting_started.md#installation) for details. + + + Textual comes with a command line application of the same name. The `textual` command is a super useful tool that will help you to build apps. Take a moment to look through the available sub-commands. There will be even more helpful tools here in the future. @@ -8,6 +16,7 @@ Take a moment to look through the available sub-commands. There will be even mor textual --help ``` + ## Run You can run Textual apps with the `run` subcommand. If you supply a path to a Python file it will load and run the application. @@ -28,9 +37,9 @@ textual run my_app.py:alternative_app ## Console -When running any terminal application, you can no longer use `print` when debugging (or log to the console). This is because anything you write to standard output would overwrite application content, making it unreadable. Fortunately Textual supplies a debug console of its own which has some super helpful features. +When running a terminal application, you will generally no longer be able to use `print` when debugging (or log to the console). This is because anything you write to standard output would overwrite application content, making it unreadable. Fortunately Textual supplies a debug console of its own which has some super helpful features. -To use the console, open up 2 terminal emulators. Run the following in one of the terminals: +To use the console, open up **two** terminal emulators. Run the following in one of the terminals: ```bash textual console @@ -41,22 +50,63 @@ You should see the Textual devtools welcome message: ```{.textual title="textual console" path="docs/examples/getting_started/console.py", press="_,_"} ``` -In the other console, run your application using `textual run` and the `--dev` switch: +In the other console, run your application with `textual run` and the `--dev` switch: ```bash textual run --dev my_app.py ``` -Anything you `print` from your application will be displayed in the console window. +Anything you `print` from your application will be displayed in the console window. Textual will also write log messages to this window which may be helpful when debugging your application. -### Textual log +### Verbosity -In addition to printing strings, Textual console supports more advanced formatting in logs. To write advanced logs import `log` from `textual` as follows: +Textual writes log messages to inform you about certain events, such as when the user presses a key or clicks on the terminal. To avoid swamping you with too much information, some events are marked as "verbose" and will be excluded from the logs. If you want to see these log messages, you can add the `-v` switch. + +```bash +textual console -v +``` + +## Textual log + +In addition to simple strings, Textual console supports [Rich](https://rich.readthedocs.io/en/latest/) formatting. To write rich logs, import `log` as follows: ```python from textual import log ``` -You can logs strings, other Python data types which will be pretty printed in the console. You can also log [Rich renderables](https://rich.readthedocs.io/en/stable/protocol.html). +This method will pretty print data structures (like lists and dicts) as well as [Rich renderables](https://rich.readthedocs.io/en/stable/protocol.html). Here are some examples: +```python +log("Hello, World") # simple string +log(locals()) # Log local variables +log(children=self.children, pi=3.141592) # key/values +log(self.tree) # Rich renderables +``` + +Textual log messages may contain [console Markup](https://rich.readthedocs.io/en/stable/markup.html): + +```python +log("[bold red]DANGER![/] We're having too much fun") +``` + +### Log method + +There's a convenient shortcut to `log` available on the App and Widget objects you can use in event handlers: + +```python +from textual.app import App + +class LogApp(App): + + def on_load(self): + self.log("In the log handler!", pi=3.141529) + + def on_mount(self): + self.log(self.tree) + +app = LogApp() +if __name__ == "__main__": + app.run() + +``` diff --git a/docs/images/layout_table.excalidraw.svg b/docs/images/layout_table.excalidraw.svg index b1c03573d..aed2f9d24 100644 --- a/docs/images/layout_table.excalidraw.svg +++ b/docs/images/layout_table.excalidraw.svg @@ -1,6 +1,6 @@ - + - eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ZW0/bWFx1MDAxMMff+ylQ+tq4Z+bcK61WNFxcWiCUkt1cdTAwMDK7WlWO41x1MDAxMINcdTAwMTNcdTAwMWLbIUDV775jhyU33KZpakXrXHUwMDA3Kz5zLmP7l5n/XHUwMDFjf3mxtVXL7mO/9mar5t95blx1MDAxOHRcdTAwMTJ3VHuVt9/6SVx1MDAxYURcdTAwMDMyYXGdRsPEK3r2sixO37x+3XeTaz+LQ9fzndsgXHUwMDFkumGaXHI7QeR4Uf91kPn99Pf8fOz2/d/iqN/JXHUwMDEyZ7JI3e9cdTAwMDRZlIzX8kO/71x1MDAwZrKUZv+brre2vlx1MDAxNOcp71x1MDAxMt/L3MFl6Fx1MDAxN1x1MDAwM1xu05SDTM23XHUwMDFlR4PCWVx1MDAwNOTaWG6fOlx1MDAwNOlcdTAwMGUtl/lcdTAwMWSydsllf2LJm2pcdTAwMTdcdTAwMDfN7bOP7frZyd1d4+P1kN1cdTAwMWPeXHUwMDFkTFbtXHUwMDA2YdjK7sPCKy+J0rTeczOvN+mRZkl07Z9cdTAwMDWdrEd9YK79aWxcdTAwMWHRg5iMSqLhZW/gp+nMmCh2vSC7z9tcdTAwMTh7alx1MDAxZD+IN1uTlruih3CEZchcdTAwMDXX8slSjFXGYahnXHJjd1x1MDAxYVFIL4HcecmKY+JQ2/WuL8mrQeepT5a4gzR2XHUwMDEzelWTfqPHXHUwMDFiXHUwMDE1Vjn5o2ZcbieL9PzgspeRlaNxzOz6fvFcblx1MDAwMFx1MDAxOFdWXHUwMDAzm1jyVeP3nYKGf+ZcdTAwMWZfz03ix8dUS/OLKY9zZ3enUJpcZlx1MDAxZcZcdTAwMWR3/MpBKURrpURE82RcdTAwMGaDwTVcdTAwMTlcdTAwMDfDMJy0Rd71hJKi9eurXHUwMDE16Fx1MDAwNFx1MDAwM2V0guBKaCNcdTAwMDCWxvNcYsxprD9fdi5cdTAwMWV6O63zXHUwMDA2unB+VYLnXHUwMDFjYrNgYqVgcqlcdTAwMDWCWVx1MDAwMFM7XHUwMDAytFXml4IpnFx1MDAxMipROYDA7HNcXGpjXHUwMDE0SKOhOi45Y2BcYk25Li79MFxm4vR5KqUujZkoKIqgxaWhvOrsXHUwMDFknkrVPd/fPYhakDWu9o6OVoGywmhcdMIxhlx1MDAxOWlcdTAwMTeipVaO1ajkNDIrQPmy60qUuFxiJKCDIGaj4Vx1MDAxM5JcdTAwMDCOkFx1MDAxY3BcdTAwMDFII1x1MDAwNCCNstXxiNYoxrTBXG54RCiPknTboJiQy1x1MDAwM3kxVKOjpOPtn7jhcFx1MDAwZmL3Pey3N1x1MDAxY0huXHUwMDFkgVKb6Xc/5lE6c5iuRmObMbkuXHUwMDFhuZVGXHUwMDE4i/p/SqOSpTRKsJIhXHUwMDEzS8PoS+1dnN6c3lxmP0Rvd0+McuuNvVxyh1FRzlRcdTAwMGLpmkhcXENcXFx1MDAwNGxTbl1cdTAwMTeJkKdpbqyqMFGvXHUwMDFkxW9cbkhrTVx1MDAxOYzSkuLnQsilYey6Ot1vXFy+P/N6fHR80N5pZp/LUnXP9XrDxN9cdTAwMDBcdTAwMWOBqlx1MDAwN2VBzMi4fCwyiprCzkrL1ZO1elx1MDAwNktkzGGP6rWYiC/SqfE/XHUwMDFkK2xxLJY5hkojToK30jJH0YpcYpVQXG6kmMsw5VxmhEa5fFx1MDAxMZ707el+eLB90k/f3orgoKWPd+NNp5RzokAzzfh8XG7PKdWG46xlvZhcdTAwMWHm6HGJP55cdTAwMDdWoVx1MDAxNCWn0GZs1ZRcbjS6XCJKVWndQ9U4ZVx1MDAxMSHN8tX4X432fnB1XHUwMDA28Knree+uXHUwMDFlLpvHO+6mcyqQ6nEln9koQkrBQuHcJs56OaVk7fBcXM4+MjjF2vKg0lx1MDAxZkqRXGJcdTAwMDNd7a6R4sZIUVxyqGxKXcyHU7p3kNOJ6HucNk+UVz+Fm/1MRkN5eNjubsNg0zlcdTAwMDXMNZ9kiyVcdTAwMTFn6Fx1MDAwMJvfUVpdi/66tE/AUEZQrFJxaiVJRlVR2kdRXrhcdTAwMDMgKVx1MDAxMMaXl6cgdlx1MDAwMqyLXHUwMDExXHUwMDBm7Fx1MDAwNV5cdTAwMWaMwtG7/t2mg8q5cJixZiGeckZllDVM/OxcdTAwMDbnNzhdT95XUkqOouK8XHUwMDBmjH5UgymXvFx1MDAxNFOhlFGag15cdTAwMWFTb7dxdXHYPKvzT6zVvv/ktnpRuOmYXG60jmTymS1Pzlx1MDAxNFnAyp/Wp+WcgrCOnT5WyftcdTAwMDCSdFx1MDAwYlx1MDAxM9XugloltGZr25X/TjyV5Vx1MDAxZjNcdTAwMTknSjmI5Vx1MDAwNWrrfK/ZxEHWYSzTf/x5XFw/yuKdTVx1MDAwNzVP/FZcdTAwMGLFXHUwMDE2QbXaoeQ2Z1lcdTAwMDVURNP2n0/8XHUwMDE2SFx1MDAxZetcdTAwMTlp8SOhNP9yXHUwMDA0lPaqJVx1MDAxNOn/W9H3TNI0paFcdTAwMTSlUcL+XGKhn/vx1Yc06D58XGa3e7fRUe9cdTAwMTQu2KZcdTAwMTOah1LEMkKZYXOidb2EUsJy7PhTgfiJXHUwMDEySoE1WHkopVxmJNRcdTAwMGaDSudi0ppcdTAwMWLHrYymJPNcdTAwMThb8jrotIJcdTAwMDd/ZprabeCP3j5XmFx1MDAxNkftxSP8OV9+7vOXry++/lx1MDAwYntetL4ifQ== + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ZW3PaRlx1MDAxNMff8yk85DVsds/eM9PpONimwZc40NZ2O52MkFx1MDAxNiQjkCxcdOM4k+/elUhcdTAwMTFcYpNQyrhcdTAwMDQ9aEZnb8e7P875n/XnXHUwMDE3XHUwMDA3XHUwMDA3texTbGpvXHUwMDBlaubBdcLAS5xJ7VVuvzdJXHUwMDFhRCPbXHUwMDA0xXdcdTAwMWGNXHUwMDEzt+jpZ1mcvnn9eugkXHUwMDAzk8Wh41x1MDAxYXRcdTAwMWakYydMs7FcdTAwMTdEyI2Gr4PMXGbTn/P3hTM0P8XR0MtcdTAwMTJULlI3XpBFyXQtXHUwMDEzmqFcdTAwMTllqZ39T/t9cPC5eM95l1x1MDAxODdzRv3QXHUwMDE0XHUwMDAziqbSQVx1MDAwZbRqvYhGhbNSYYEpV3rWIUiP7HKZ8Wxrz7psypbcVLtpnVx1MDAxZl596NavLlx1MDAxZlx1MDAxZVx1MDAxYVx1MDAxZlx1MDAwNmN8d/rQKlftXHUwMDA1YdjJPoWFV25cdTAwMTKlad13Mtcve6RZXHUwMDEyXHLMVeBlvu1DKvbZ2DSyXHUwMDFiUY5KonHfXHUwMDFmmTRdXHUwMDE4XHUwMDEzxY5cdTAwMWJkn3JcdTAwMWLGM+t0I95cdTAwMWOUloeiXHUwMDA3Q1RSzISSfNZSjFx1MDAxNVxuXHUwMDExxjglwCvuNKLQXHUwMDFlgnXnJS6e0qGu41x1MDAwZfrWq5E365MlziiNncRcdTAwMWVV2W/y9Vx1MDAwZmVa2OWVxmJuXHUwMDEx31x1MDAwNH0/s61cdTAwMTRcdTAwMTRSjM45lpriXGKIXHUwMDEyUlGQsjzCfNX4nVfQ8Fd1+3wnib9uUy3NP+Y8zp09nkOpXHUwMDFjPI49Z3rkRFxioFhcYo0xLfcvXGZGXHUwMDAz2zhcdTAwMWGHYWmL3EFJSWH98mpcdTAwMDM6iYaVdFxuKblQhKxN51x1MDAxOVHtWH7sezeP/lHnulx1MDAwMVx1MDAwZbm+XUFnhbBFLuFZuVx1MDAwNFxuXGbIMpdcdTAwMTIxXCL1XHUwMDAysNvnkqFcdTAwMTVQgkBcdTAwMDRcYtZPYSmVXHUwMDEyhCtJfmAsTVx1MDAxOFx1MDAwNnH6NJRCroKSgKCSYGB8bSpvvZPTNlx1MDAxN73r5nEr6pCscXtydrZcdJXPXHUwMDE4LVx0Q0phxVx1MDAxN06/XHUwMDE4K1x1MDAwNdJcdTAwMTJcdTAwMDRcdTAwMTf/LVq+7DlcdTAwMWM4LFx1MDAxM0lcdTAwMDBcdTAwMDFhi9FwxiQhqFx1MDAxYainRCrGXGLYUfr5gSTPXHUwMDAwJFx1MDAwMFlccqSywYNpvj6QN2MxOUs8t3nphONcdTAwMTNcdTAwMTI770izu+NAUo1cdTAwMThwqebPfsojR1x1MDAxNUw3o7GLMd9cdTAwMTaNVHPFlFx1MDAwNrmnNM5tRpVGLG101DZGrk2j4dK9ad+178bvo7fHl0o49cbJjtMobNZcXFx1MDAxNpJcdTAwMTbFLVx1MDAwNEZcdTAwMDJdm123hVwiyVx1MDAxMzVVWvxcdTAwMGapemssfltBXHUwMDEyvlJCMlxuQtn6Zv1cdTAwMDKn58i02ei/u3J9OrlodY/Os4+rkrXvuP44MTvAI1x1MDAxMMujkMtMgqVmSV1unq7FU1xcaoxA0pzL6UR0XHUwMDE5T1x0/0hZpotnXHRTomxxROeF195hOldmV6OmxsDtr1x1MDAxNNbnNFx1MDAxOep2M2xcdTAwMWReXHUwMDBl07f3LGh15MVxvOucUmo5kFhiWk3jgDXKz2KxXHUwMDE22S6oXG4jOc8p2YRT4FQrofT+ckphdfGDqS3KQdP1Of2j0W1cdTAwMDa3V4T83nPdX25cdTAwMWb751x1MDAxN0fOrnNq01x1MDAwNlJSaL7MqY2nXFxhvChEt1x1MDAxY1CZLb6YzJXElEK5XGZqrkB4cZc1XVx1MDAwYosqqfZcdTAwMTclOFx1MDAwNjJcdTAwMTf3941UXHUwMDAyeCWpXHUwMDAyXHUwMDEzRYGT9XXo+aVw621y18x4NOanp93eIVx1MDAxOe06qXnmZ5zj5cKIYkBWjW8h9U9cdTAwMDXp06mfXCLNXGLLg/bmqZ8qZXOCwPurUC2H30j9Np3k97xrg0rYUVx1MDAwMHU2oYG+gUFrXHUwMDEyTn5cdTAwMTk+7DqolDKElV4sX6ac2lpKq8rF/HY53U7mXHUwMDE33Go0YPub+blcdTAwMTIrMZVC2OPjZP1bps71yfk5jDJcdTAwMGbjTP7620X9LIuPdlx1MDAxZNOikpJM4KWLT6ol4lpXWjbhXHUwMDE0QHXNk5yCJohcbrlcdTAwMTCx/1xyofm9PFx1MDAxMXR/XHUwMDAzKdPf0Ka5XHUwMDFlgLl7ju9cdTAwMDH6cVx1MDAxON++T4Pe44fw0L+Pzvw2ucG7XHUwMDBlKLNxlC39X2ZcdTAwMDboVqTpakBcdFx1MDAxM0hPr2FXStPvclxuWFx1MDAxMK2A/Vx1MDAxOJf19l1MWnPiuJPZKW3zlFrrdeB1gkezME3tPjCTt09cdP7iqb34yn7Ol8l9/vzlxZe/XHUwMDAxUO5ccsMifQ== - \ No newline at end of file + \ No newline at end of file diff --git a/docs/reference/button.md b/docs/reference/button.md new file mode 100644 index 000000000..26d6b63cf --- /dev/null +++ b/docs/reference/button.md @@ -0,0 +1 @@ +::: textual.widgets.Button diff --git a/docs/widgets/button.md b/docs/widgets/button.md new file mode 100644 index 000000000..88423dcb5 --- /dev/null +++ b/docs/widgets/button.md @@ -0,0 +1 @@ +# Button diff --git a/docs/widgets/data_table.md b/docs/widgets/data_table.md new file mode 100644 index 000000000..580cba85f --- /dev/null +++ b/docs/widgets/data_table.md @@ -0,0 +1 @@ +# DataTable diff --git a/docs/widgets/footer.md b/docs/widgets/footer.md new file mode 100644 index 000000000..571fbcaac --- /dev/null +++ b/docs/widgets/footer.md @@ -0,0 +1 @@ +# Footer diff --git a/docs/widgets/header.md b/docs/widgets/header.md new file mode 100644 index 000000000..6c67b6a97 --- /dev/null +++ b/docs/widgets/header.md @@ -0,0 +1 @@ +# Header diff --git a/docs/widgets/static.md b/docs/widgets/static.md new file mode 100644 index 000000000..0be60b558 --- /dev/null +++ b/docs/widgets/static.md @@ -0,0 +1 @@ +# Static diff --git a/docs/widgets/tabs.md b/docs/widgets/tabs.md deleted file mode 100644 index 2f1151c7a..000000000 --- a/docs/widgets/tabs.md +++ /dev/null @@ -1 +0,0 @@ -::: textual.widgets.tabs.Tabs diff --git a/docs/widgets/tree_control.md b/docs/widgets/tree_control.md new file mode 100644 index 000000000..1155acfcc --- /dev/null +++ b/docs/widgets/tree_control.md @@ -0,0 +1 @@ +# TreeControl diff --git a/mkdocs.yml b/mkdocs.yml index 46367e352..7b413fd24 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,7 @@ nav: - "getting_started.md" - "introduction.md" - Guide: + - "guide/app.md" - "guide/devtools.md" - "guide/layout.md" - "guide/CSS.md" @@ -68,9 +69,16 @@ nav: - "styles/tint.md" - "styles/visibility.md" - "styles/width.md" - - Widgets: "/widgets/" + - Widgets: + - "widgets/button.md" + - "widgets/data_table.md" + - "widgets/footer.md" + - "widgets/header.md" + - "widgets/static.md" + - "widgets/tree_control.md" - Reference: - "reference/app.md" + - "reference/button.md" - "reference/color.md" - "reference/dom_node.md" - "reference/events.md" @@ -98,6 +106,7 @@ markdown_extensions: custom_checkbox: true - pymdownx.highlight: anchor_linenums: true + - pymdownx.inlinehilite - pymdownx.superfences: custom_fences: - name: textual diff --git a/src/textual/__init__.py b/src/textual/__init__.py index 7be25a7f4..53a57495c 100644 --- a/src/textual/__init__.py +++ b/src/textual/__init__.py @@ -92,6 +92,11 @@ class Logger: """An error logger.""" return Logger(LogGroup.ERROR) + @property + def system(self) -> Logger: + """A system logger.""" + return Logger(LogGroup.SYSTEM) + log = Logger() diff --git a/src/textual/_log.py b/src/textual/_log.py index ec9674cda..97211b807 100644 --- a/src/textual/_log.py +++ b/src/textual/_log.py @@ -11,6 +11,7 @@ class LogGroup(Enum): WARNING = 4 ERROR = 5 PRINT = 6 + SYSTEM = 7 class LogVerbosity(Enum): diff --git a/src/textual/app.py b/src/textual/app.py index 04e92e20c..cf55a57b3 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -143,7 +143,6 @@ class App(Generic[ReturnType], DOMNode): Args: driver_class (Type[Driver] | None, optional): Driver class or ``None`` to auto-detect. Defaults to None. - log_verbosity (int, optional): Log verbosity from 0-3. Defaults to 1. title (str | None, optional): Title of the application. If ``None``, the title is set to the name of the ``App`` subclass. Defaults to ``None``. css_path (str | PurePath | None, optional): Path to CSS or ``None`` for no CSS file. Defaults to None. watch_css (bool, optional): Watch CSS for changes. Defaults to False. @@ -693,7 +692,9 @@ class App(Generic[ReturnType], DOMNode): stylesheet.read(self.css_path) stylesheet.parse() elapsed = (perf_counter() - time) * 1000 - self.log(f" loaded {self.css_path!r} in {elapsed:.0f} ms") + self.log.system( + f" loaded {self.css_path!r} in {elapsed:.0f} ms" + ) except Exception as error: # TODO: Catch specific exceptions self.log.error(error) @@ -803,10 +804,10 @@ class App(Generic[ReturnType], DOMNode): """ screen.post_message_no_wait(events.ScreenSuspend(self)) - self.log(f"{screen} SUSPENDED") + self.log.system(f"{screen} SUSPENDED") if not self.is_screen_installed(screen) and screen not in self._screen_stack: screen.remove() - self.log(f"{screen} REMOVED") + self.log.system(f"{screen} REMOVED") return screen def push_screen(self, screen: Screen | str) -> None: @@ -819,7 +820,7 @@ class App(Generic[ReturnType], DOMNode): next_screen = self.get_screen(screen) self._screen_stack.append(next_screen) self.screen.post_message_no_wait(events.ScreenResume(self)) - self.log(f"{self.screen} is current (PUSHED)") + self.log.system(f"{self.screen} is current (PUSHED)") def switch_screen(self, screen: Screen | str) -> None: """Switch to a another screen by replacing the top of the screen stack with a new screen. @@ -833,7 +834,7 @@ class App(Generic[ReturnType], DOMNode): next_screen = self.get_screen(screen) self._screen_stack.append(next_screen) self.screen.post_message_no_wait(events.ScreenResume(self)) - self.log(f"{self.screen} is current (SWITCHED)") + self.log.system(f"{self.screen} is current (SWITCHED)") def install_screen(self, screen: Screen, name: str | None = None) -> str: """Install a screen. @@ -859,7 +860,7 @@ class App(Generic[ReturnType], DOMNode): ) self._installed_screens[name] = screen self.get_screen(name) # Ensures screen is running - self.log(f"{screen} INSTALLED name={name!r}") + self.log.system(f"{screen} INSTALLED name={name!r}") return name def uninstall_screen(self, screen: Screen | str) -> str | None: @@ -879,7 +880,7 @@ class App(Generic[ReturnType], DOMNode): if uninstall_screen in self._screen_stack: raise ScreenStackError("Can't uninstall screen in screen stack") del self._installed_screens[screen] - self.log(f"{uninstall_screen} UNINSTALLED name={screen!r}") + self.log.system(f"{uninstall_screen} UNINSTALLED name={screen!r}") return screen else: if screen in self._screen_stack: @@ -887,7 +888,7 @@ class App(Generic[ReturnType], DOMNode): for name, installed_screen in self._installed_screens.items(): if installed_screen is screen: self._installed_screens.pop(name) - self.log(f"{screen} UNINSTALLED name={name!r}") + self.log.system(f"{screen} UNINSTALLED name={name!r}") return name return None @@ -905,7 +906,7 @@ class App(Generic[ReturnType], DOMNode): previous_screen = self._replace_screen(screen_stack.pop()) self.screen._screen_resized(self.size) self.screen.post_message_no_wait(events.ScreenResume(self)) - self.log(f"{self.screen} is active") + self.log.system(f"{self.screen} is active") return previous_screen def set_focus(self, widget: Widget | None) -> None: @@ -1045,15 +1046,15 @@ class App(Generic[ReturnType], DOMNode): if self.devtools_enabled: try: await self.devtools.connect() - self.log(f"Connected to devtools ( {self.devtools.url} )") + self.log.system(f"Connected to devtools ( {self.devtools.url} )") except DevtoolsConnectionError: - self.log(f"Couldn't connect to devtools ( {self.devtools.url} )") + self.log.system(f"Couldn't connect to devtools ( {self.devtools.url} )") - self.log("---") + self.log.system("---") - self.log(driver=self.driver_class) - self.log(loop=asyncio.get_running_loop()) - self.log(features=self.features) + self.log.system(driver=self.driver_class) + self.log.system(loop=asyncio.get_running_loop()) + self.log.system(features=self.features) try: if self.css_path is not None: @@ -1079,7 +1080,7 @@ class App(Generic[ReturnType], DOMNode): if self.css_monitor: self.set_interval(0.25, self.css_monitor, name="css monitor") - self.log("[b green]STARTED[/]", self.css_monitor) + self.log.system("[b green]STARTED[/]", self.css_monitor) process_messages = super()._process_messages diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index 552e5109e..447110325 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -21,6 +21,7 @@ __all__ = [ "Static", "TextInput", "TreeControl", + "Welcome", ] diff --git a/src/textual/widgets/__init__.pyi b/src/textual/widgets/__init__.pyi index 40c986a59..49537d599 100644 --- a/src/textual/widgets/__init__.pyi +++ b/src/textual/widgets/__init__.pyi @@ -9,3 +9,4 @@ from ._pretty import Pretty as Pretty from ._static import Static as Static from ._text_input import TextInput as TextInput from ._tree_control import TreeControl as TreeControl +from ._welcome import Welcome as Welcome diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index 68c50839a..f8fcc229f 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -32,7 +32,8 @@ class Button(Widget, can_focus=True): DEFAULT_CSS = """ Button { width: auto; - min-width: 10; + min-width: 16; + width: auto; height: 3; background: $panel; color: $text-panel; diff --git a/src/textual/widgets/_welcome.py b/src/textual/widgets/_welcome.py new file mode 100644 index 000000000..ac390f831 --- /dev/null +++ b/src/textual/widgets/_welcome.py @@ -0,0 +1,58 @@ +from ..app import ComposeResult +from ._static import Static +from ._button import Button +from ..layout import Container + +from rich.markdown import Markdown + +WELCOME_MD = """\ +# Welcome! + +Textual is a TUI, or *Text User Interface*, framework for Python inspired by modern web development. **We hope you enjoy using Textual!** + +## Dune quote + +> "I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +I will permit it to pass over me and through me. +And when it has gone past, I will turn the inner eye to see its path. +Where the fear has gone there will be nothing. Only I will remain." + +""" + + +class Welcome(Static): + + DEFAULT_CSS = """ + + Welcome { + width: 100%; + height: 100%; + margin: 1 2; + + } + + Welcome Container { + padding: 1; + background: $panel; + color: $text-panel; + } + + Welcome #text { + margin: 0 1; + } + + Welcome #close { + dock: bottom; + width: 100%; + margin-top: 1; + } + + """ + + def compose(self) -> ComposeResult: + + yield Container(Static(Markdown(WELCOME_MD), id="text"), id="md") + yield Button("OK", id="close", variant="success") From d945029b9a7216aa1f633ff2d3151ff776c3a61d Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 4 Sep 2022 21:10:15 +0100 Subject: [PATCH 03/24] mounting --- docs/examples/app/widgets02.py | 15 +++++++++++++++ docs/guide/app.md | 17 ++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 docs/examples/app/widgets02.py diff --git a/docs/examples/app/widgets02.py b/docs/examples/app/widgets02.py new file mode 100644 index 000000000..9fb52c228 --- /dev/null +++ b/docs/examples/app/widgets02.py @@ -0,0 +1,15 @@ +from textual.app import App +from textual.widgets import Welcome + + +class WelcomeApp(App): + def on_key(self) -> None: + self.mount(Welcome()) + + def on_button_pressed(self) -> None: + self.exit() + + +app = WelcomeApp() +if __name__ == "__main__": + app.run() diff --git a/docs/guide/app.md b/docs/guide/app.md index f578b5a5c..5e029e72c 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -88,7 +88,22 @@ The following example imports a builtin Welcome widget and yields it from compos When you run this code, Textual will *mount* the Welcome widget which contains a Markdown content area and a button: -```{.textual path="docs/examples/app/widgets01.py"} +```{.textual path="docs/examples/app/widgets01.py" title="widgets01.py" } ``` Notice the `on_button_pressed` method which handles the [Button.Pressed][textual.widgets.Button] event send by the button contained in the Welcome widget. The handlers calls [App.exit()][textual.app.App] to exit the app. + +### Mounting + +While composing is the preferred way of adding widgets when your app starts it is sometimes necessary to add new widget(s) in response to events. You can do this by calling [mount()](textual.widget.Widget.mount) which will add a new widget to the UI. + +Here's an app which adds the welcome widget in response to any key press: + +```python title="widgets02.py" +--8<-- "docs/examples/app/widgets02.py" +``` + +When you first run this you will get a blank screen. Press any key to add the welcome widget. You can even press a key multiple times to add several widgets. + +```{.textual path="docs/examples/app/widgets02.py" title="widgets02.py" press="a,a,a,down,down,down,down,down,down,_,_,_,_,_,_"} +``` From 7db856d4044a7de7434676b33811cbba595c17b3 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 5 Sep 2022 10:51:19 +0100 Subject: [PATCH 04/24] return types and buttons --- docs/examples/app/question01.py | 18 ++++++++++++++ docs/examples/app/question02.py | 20 +++++++++++++++ docs/guide/app.md | 31 +++++++++++++++++++++++ src/textual/_compose.py | 44 +++++++++++++++++++++++++++++++++ src/textual/app.py | 28 ++++++++------------- src/textual/widget.py | 31 +++++++++++------------ src/textual/widgets/_button.py | 26 ++++++++++++++----- 7 files changed, 157 insertions(+), 41 deletions(-) create mode 100644 docs/examples/app/question01.py create mode 100644 docs/examples/app/question02.py create mode 100644 src/textual/_compose.py diff --git a/docs/examples/app/question01.py b/docs/examples/app/question01.py new file mode 100644 index 000000000..01a70c233 --- /dev/null +++ b/docs/examples/app/question01.py @@ -0,0 +1,18 @@ +from textual.app import App, ComposeResult +from textual.widgets import Static, Button + + +class QuestionApp(App[str]): + def compose(self) -> ComposeResult: + yield Static("Do you love Textual?") + yield Button("Yes", id="yes", variant="primary") + yield Button("No", id="no", variant="error") + + def on_button_pressed(self, event: Button.Pressed) -> None: + self.exit(event.button.id) + + +app = QuestionApp() +if __name__ == "__main__": + reply = app.run() + print(reply) diff --git a/docs/examples/app/question02.py b/docs/examples/app/question02.py new file mode 100644 index 000000000..444eaebae --- /dev/null +++ b/docs/examples/app/question02.py @@ -0,0 +1,20 @@ +from textual.app import App, ComposeResult +from textual.widgets import Static, Button + + +class QuestionApp(App[str]): + def compose(self) -> ComposeResult: + yield Static("Do you love Textual?") + yield (yes := Button("Yes", id="yes")) + yes.variant = "primary" + yield (no := Button("No", id="no")) + no.variant = "error" + + def on_button_pressed(self, event: Button.Pressed) -> None: + self.exit(event.button.id) + + +app = QuestionApp() +if __name__ == "__main__": + reply = app.run() + print(reply) diff --git a/docs/guide/app.md b/docs/guide/app.md index 5e029e72c..35aff27c0 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -107,3 +107,34 @@ When you first run this you will get a blank screen. Press any key to add the we ```{.textual path="docs/examples/app/widgets02.py" title="widgets02.py" press="a,a,a,down,down,down,down,down,down,_,_,_,_,_,_"} ``` + +### Exiting + +An app will run until you call [App.exit()](textual.app.App.exit) which will exit application mode and the [run](textual.app.App.run) method will return. If this is the last line in your code you will return to the command prompt. + +The exit method will also accept an optional positional value to be returned by `run()`. The following example uses this to return the `id` (identifier) of a clicked button. + +```python title="question01.py" +--8<-- "docs/examples/app/question01.py" +``` + +Running this app will give you the following: + +```{.textual path="docs/examples/app/question01.py"} +``` + +Clicking either of those buttons will exit the app, and the `run()` method will return either `"yes"` or `"no"` depending on button clicked. + +#### Typing + +You may have noticed that we subclassed `App[str]` rather than the usual `App`. + +```python title="question01.py" hl_lines="5" +--8<-- "docs/examples/app/question01.py" +``` + +The addition of `[str]` tells Mypy that `run()` is expected to return a string. It may also return `None` if `sys.exit()` is called without a return value, so the return type of `run` will be `str | None`. + +!!! note + + Type annotations are entirely optional (but recommended) with Textual. diff --git a/src/textual/_compose.py b/src/textual/_compose.py new file mode 100644 index 000000000..9e61cf577 --- /dev/null +++ b/src/textual/_compose.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from types import GeneratorType +from typing import TYPE_CHECKING, Iterable + +if TYPE_CHECKING: + + from .app import ComposeResult + from .widget import Widget + + +def _compose(compose_result: ComposeResult) -> Iterable[Widget]: + """Turns a compose result in to an iterable of Widgets. + + If `compose_result` is a generator, this will run the generator and send + back the yielded widgets. This allows you to write code such as this: + + ```python + yes = yield Button("Yes") + yes.variant = "success" + ``` + + Otherwise `compose_result` is assumed to already be an iterable of Widgets + and will be returned unmodified. + + Args: + compose_result (ComposeResult): Either an iterator of widgets, + or a generator. + + Returns: + Iterable[Widget]: In iterable if widgets. + + """ + + if not isinstance(compose_result, GeneratorType): + return compose_result + + try: + widget = next(compose_result) + while True: + yield widget + widget = compose_result.send(widget) + except StopIteration: + pass diff --git a/src/textual/app.py b/src/textual/app.py index cf55a57b3..8ba711ddf 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -9,21 +9,13 @@ import sys import warnings from contextlib import redirect_stderr, redirect_stdout from datetime import datetime -from pathlib import PurePath, Path +from pathlib import Path, PurePath from time import perf_counter -from typing import ( - Any, - Generic, - Iterable, - Iterator, - TextIO, - Type, - TypeVar, - cast, -) +from typing import Any, Generator, Generic, Iterable, Iterator, Type, TypeVar, cast from weakref import WeakSet, WeakValueDictionary from ._ansi_sequences import SYNC_END, SYNC_START +from ._compose import _compose from ._path import _make_path_object_relative if sys.version_info >= (3, 8): @@ -42,8 +34,8 @@ from rich.traceback import Traceback from . import ( Logger, - LogSeverity, LogGroup, + LogSeverity, LogVerbosity, actions, events, @@ -72,7 +64,6 @@ from .renderables.blank import Blank from .screen import Screen from .widget import Widget - PLATFORM = platform.system() WINDOWS = PLATFORM == "Windows" @@ -107,7 +98,7 @@ DEFAULT_COLORS = { } -ComposeResult = Iterable[Widget] +ComposeResult = Iterable[Widget] | Generator[Widget, Widget, None] class AppError(Exception): @@ -395,6 +386,9 @@ class App(Generic[ReturnType], DOMNode): return yield + def _compose(self) -> Iterable[Widget]: + return _compose(self.compose()) + def get_css_variables(self) -> dict[str, str]: """Get a mapping of variables used to pre-populate CSS. @@ -1155,7 +1149,7 @@ class App(Generic[ReturnType], DOMNode): self.set_timer(screenshot_timer, on_screenshot, name="screenshot timer") def _on_mount(self) -> None: - widgets = self.compose() + widgets = self._compose() if widgets: self.mount_all(widgets) @@ -1189,9 +1183,7 @@ class App(Generic[ReturnType], DOMNode): parent (Widget): Parent Widget """ if not anon_widgets and not widgets: - raise AppError( - "Nothing to mount, did you forget parent as first positional arg?" - ) + return name_widgets: Iterable[tuple[str | None, Widget]] name_widgets = [*((None, widget) for widget in anon_widgets), *widgets.items()] apply_stylesheet = self.stylesheet.apply diff --git a/src/textual/widget.py b/src/textual/widget.py index a42f5c833..63a332447 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -4,16 +4,11 @@ from asyncio import Lock from fractions import Fraction from itertools import islice from operator import attrgetter -from typing import ( - TYPE_CHECKING, - ClassVar, - Collection, - Iterable, - NamedTuple, -) +from types import GeneratorType +from typing import TYPE_CHECKING, ClassVar, Collection, Iterable, NamedTuple import rich.repr -from rich.console import Console, RenderableType, JustifyMethod +from rich.console import Console, JustifyMethod, RenderableType from rich.measure import Measurement from rich.segment import Segment from rich.style import Style @@ -22,7 +17,8 @@ from rich.text import Text from . import errors, events, messages from ._animator import BoundAnimator -from ._arrange import arrange, DockArrangeResult +from ._arrange import DockArrangeResult, arrange +from ._compose import _compose from ._context import active_app from ._layout import Layout from ._segment_tools import align_lines @@ -30,8 +26,7 @@ from ._styles_cache import StylesCache from ._types import Lines from .box_model import BoxModel, get_box_model from .css.constants import VALID_TEXT_ALIGN -from .dom import DOMNode -from .dom import NoScreen +from .dom import DOMNode, NoScreen from .geometry import Offset, Region, Size, Spacing, clamp from .layouts.vertical import VerticalLayout from .message import Message @@ -41,12 +36,12 @@ if TYPE_CHECKING: from .app import App, ComposeResult from .scrollbar import ( ScrollBar, + ScrollBarCorner, ScrollDown, ScrollLeft, ScrollRight, ScrollTo, ScrollUp, - ScrollBarCorner, ) @@ -239,7 +234,7 @@ class Widget(DOMNode): # reset the scroll position if the scrollbar is hidden. self.scroll_to(0, 0, animate=False) - def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None: + def mount(self, *anon_widgets: Widget, **widgets: Widget) -> int: """Mount child widgets (making this widget a container). Widgets may be passed as positional arguments or keyword arguments. If keyword arguments, @@ -273,6 +268,9 @@ class Widget(DOMNode): return yield + def _compose(self) -> Iterable[Widget]: + return _compose(self.compose()) + def _post_register(self, app: App) -> None: """Called when the instance is registered. @@ -1458,10 +1456,9 @@ class Widget(DOMNode): await self.dispatch_key(event) def _on_mount(self, event: events.Mount) -> None: - widgets = list(self.compose()) - if widgets: - self.mount(*widgets) - self.screen.refresh(repaint=False, layout=True) + widgets = self._compose() + self.mount(*widgets) + self.screen.refresh(repaint=False, layout=True) def _on_leave(self, event: events.Leave) -> None: self.mouse_over = False diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index f8fcc229f..d2088cfc9 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -44,6 +44,10 @@ class Button(Widget, can_focus=True): text-style: bold; } + Button.-disabled { + opacity: 0.6; + } + Button:focus { text-style: bold reverse; } @@ -183,22 +187,32 @@ class Button(Widget, can_focus=True): if label is None: label = self.css_identifier_styled - self.label: Text = label + self.label = label self.disabled = disabled if disabled: self.add_class("-disabled") - if variant in _VALID_BUTTON_VARIANTS: - if variant != "default": - self.add_class(f"-{variant}") + self.variant = variant - else: + label: Reactive[RenderableType] = Reactive("") + variant = Reactive.init("default") + disabled = Reactive(False) + + def validate_variant(self, variant: str) -> str: + if variant not in _VALID_BUTTON_VARIANTS: raise InvalidButtonVariant( f"Valid button variants are {friendly_list(_VALID_BUTTON_VARIANTS)}" ) + return variant - label: Reactive[RenderableType] = Reactive("") + def watch_variant(self, old_variant: str, variant: str): + self.remove_class(f"_{old_variant}") + self.add_class(f"-{variant}") + + def watch_disabled(self, disabled: bool) -> None: + self.set_class(disabled, "-disabled") + self.can_focus = not disabled def validate_label(self, label: RenderableType) -> RenderableType: """Parse markup for self.label""" From 54539a90d10678116864e0bbaa999b920b6412f4 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 5 Sep 2022 11:02:37 +0100 Subject: [PATCH 05/24] refine compose --- docs/examples/app/question02.py | 4 ++-- src/textual/_compose.py | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/examples/app/question02.py b/docs/examples/app/question02.py index 444eaebae..7efb0f2f6 100644 --- a/docs/examples/app/question02.py +++ b/docs/examples/app/question02.py @@ -5,9 +5,9 @@ from textual.widgets import Static, Button class QuestionApp(App[str]): def compose(self) -> ComposeResult: yield Static("Do you love Textual?") - yield (yes := Button("Yes", id="yes")) + yes = yield Button("Yes", id="yes") yes.variant = "primary" - yield (no := Button("No", id="no")) + no = yield Button("No", id="no") no.variant = "error" def on_button_pressed(self, event: Button.Pressed) -> None: diff --git a/src/textual/_compose.py b/src/textual/_compose.py index 9e61cf577..0a2a01754 100644 --- a/src/textual/_compose.py +++ b/src/textual/_compose.py @@ -4,7 +4,6 @@ from types import GeneratorType from typing import TYPE_CHECKING, Iterable if TYPE_CHECKING: - from .app import ComposeResult from .widget import Widget @@ -28,17 +27,18 @@ def _compose(compose_result: ComposeResult) -> Iterable[Widget]: or a generator. Returns: - Iterable[Widget]: In iterable if widgets. + Iterable[Widget]: An iterable if widgets. """ - if not isinstance(compose_result, GeneratorType): - return compose_result - - try: - widget = next(compose_result) - while True: - yield widget - widget = compose_result.send(widget) - except StopIteration: - pass + if isinstance(compose_result, GeneratorType): + try: + widget = next(compose_result) + send = compose_result.send + while True: + yield widget + widget = send(widget) + except StopIteration: + pass + else: + yield from compose_result From e7d57a4aa286456ec76ed2895cfc88f602d2a987 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 5 Sep 2022 21:30:29 +0100 Subject: [PATCH 06/24] renamed introduction to tutorial --- docs/examples/app/question02.css | 17 +++++ docs/examples/app/question02.py | 10 ++- docs/examples/app/question03.py | 38 +++++++++++ .../{introduction => tutorial}/clock01.py | 0 .../{introduction => tutorial}/clock02.py | 0 .../{introduction => tutorial}/intro01.py | 0 .../{introduction => tutorial}/intro02.py | 0 .../{introduction => tutorial}/stopwatch.css | 0 .../{introduction => tutorial}/stopwatch.py | 0 .../{introduction => tutorial}/stopwatch01.py | 0 .../stopwatch02.css | 0 .../{introduction => tutorial}/stopwatch02.py | 0 .../stopwatch03.css | 0 .../{introduction => tutorial}/stopwatch03.py | 0 .../stopwatch04.css | 0 .../{introduction => tutorial}/stopwatch04.py | 0 .../{introduction => tutorial}/stopwatch05.py | 0 .../{introduction => tutorial}/stopwatch06.py | 0 docs/guide/CSS.md | 8 +-- docs/guide/actions.md | 1 + docs/guide/app.md | 63 +++++++++++++---- docs/guide/devtools.md | 20 +++++- docs/guide/reactivity.md | 1 + docs/guide/screens.md | 1 + docs/guide/widgets.md | 1 + docs/{introduction.md => tutorial.md} | 68 +++++++++---------- mkdocs.yml | 10 ++- src/textual/widgets/_button.py | 11 +-- 28 files changed, 182 insertions(+), 67 deletions(-) create mode 100644 docs/examples/app/question02.css create mode 100644 docs/examples/app/question03.py rename docs/examples/{introduction => tutorial}/clock01.py (100%) rename docs/examples/{introduction => tutorial}/clock02.py (100%) rename docs/examples/{introduction => tutorial}/intro01.py (100%) rename docs/examples/{introduction => tutorial}/intro02.py (100%) rename docs/examples/{introduction => tutorial}/stopwatch.css (100%) rename docs/examples/{introduction => tutorial}/stopwatch.py (100%) rename docs/examples/{introduction => tutorial}/stopwatch01.py (100%) rename docs/examples/{introduction => tutorial}/stopwatch02.css (100%) rename docs/examples/{introduction => tutorial}/stopwatch02.py (100%) rename docs/examples/{introduction => tutorial}/stopwatch03.css (100%) rename docs/examples/{introduction => tutorial}/stopwatch03.py (100%) rename docs/examples/{introduction => tutorial}/stopwatch04.css (100%) rename docs/examples/{introduction => tutorial}/stopwatch04.py (100%) rename docs/examples/{introduction => tutorial}/stopwatch05.py (100%) rename docs/examples/{introduction => tutorial}/stopwatch06.py (100%) create mode 100644 docs/guide/actions.md create mode 100644 docs/guide/reactivity.md create mode 100644 docs/guide/screens.md create mode 100644 docs/guide/widgets.md rename docs/{introduction.md => tutorial.md} (84%) diff --git a/docs/examples/app/question02.css b/docs/examples/app/question02.css new file mode 100644 index 000000000..8145fdd72 --- /dev/null +++ b/docs/examples/app/question02.css @@ -0,0 +1,17 @@ +Screen { + layout: table; + table-size: 2; + table-gutter: 2; + padding: 2; +} +#question { + width: 100%; + height: 100%; + column-span: 2; + content-align: center bottom; + text-style: bold; +} + +Button { + width: 100%; +} diff --git a/docs/examples/app/question02.py b/docs/examples/app/question02.py index 7efb0f2f6..a1d107c04 100644 --- a/docs/examples/app/question02.py +++ b/docs/examples/app/question02.py @@ -4,17 +4,15 @@ from textual.widgets import Static, Button class QuestionApp(App[str]): def compose(self) -> ComposeResult: - yield Static("Do you love Textual?") - yes = yield Button("Yes", id="yes") - yes.variant = "primary" - no = yield Button("No", id="no") - no.variant = "error" + yield Static("Do you love Textual?", id="question") + yield Button("Yes", id="yes", variant="primary") + yield Button("No", id="no", variant="error") def on_button_pressed(self, event: Button.Pressed) -> None: self.exit(event.button.id) -app = QuestionApp() +app = QuestionApp(css_path="question02.css") if __name__ == "__main__": reply = app.run() print(reply) diff --git a/docs/examples/app/question03.py b/docs/examples/app/question03.py new file mode 100644 index 000000000..1f2f02cd3 --- /dev/null +++ b/docs/examples/app/question03.py @@ -0,0 +1,38 @@ +from textual.app import App, ComposeResult +from textual.widgets import Static, Button + + +class QuestionApp(App[str]): + CSS = """ + Screen { + layout: table; + table-size: 2; + table-gutter: 2; + padding: 2; + } + #question { + width: 100%; + height: 100%; + column-span: 2; + content-align: center bottom; + text-style: bold; + } + + Button { + width: 100%; + } + """ + + def compose(self) -> ComposeResult: + yield Static("Do you love Textual?", id="question") + yield Button("Yes", id="yes", variant="primary") + yield Button("No", id="no", variant="error") + + def on_button_pressed(self, event: Button.Pressed) -> None: + self.exit(event.button.id) + + +app = QuestionApp() +if __name__ == "__main__": + reply = app.run() + print(reply) diff --git a/docs/examples/introduction/clock01.py b/docs/examples/tutorial/clock01.py similarity index 100% rename from docs/examples/introduction/clock01.py rename to docs/examples/tutorial/clock01.py diff --git a/docs/examples/introduction/clock02.py b/docs/examples/tutorial/clock02.py similarity index 100% rename from docs/examples/introduction/clock02.py rename to docs/examples/tutorial/clock02.py diff --git a/docs/examples/introduction/intro01.py b/docs/examples/tutorial/intro01.py similarity index 100% rename from docs/examples/introduction/intro01.py rename to docs/examples/tutorial/intro01.py diff --git a/docs/examples/introduction/intro02.py b/docs/examples/tutorial/intro02.py similarity index 100% rename from docs/examples/introduction/intro02.py rename to docs/examples/tutorial/intro02.py diff --git a/docs/examples/introduction/stopwatch.css b/docs/examples/tutorial/stopwatch.css similarity index 100% rename from docs/examples/introduction/stopwatch.css rename to docs/examples/tutorial/stopwatch.css diff --git a/docs/examples/introduction/stopwatch.py b/docs/examples/tutorial/stopwatch.py similarity index 100% rename from docs/examples/introduction/stopwatch.py rename to docs/examples/tutorial/stopwatch.py diff --git a/docs/examples/introduction/stopwatch01.py b/docs/examples/tutorial/stopwatch01.py similarity index 100% rename from docs/examples/introduction/stopwatch01.py rename to docs/examples/tutorial/stopwatch01.py diff --git a/docs/examples/introduction/stopwatch02.css b/docs/examples/tutorial/stopwatch02.css similarity index 100% rename from docs/examples/introduction/stopwatch02.css rename to docs/examples/tutorial/stopwatch02.css diff --git a/docs/examples/introduction/stopwatch02.py b/docs/examples/tutorial/stopwatch02.py similarity index 100% rename from docs/examples/introduction/stopwatch02.py rename to docs/examples/tutorial/stopwatch02.py diff --git a/docs/examples/introduction/stopwatch03.css b/docs/examples/tutorial/stopwatch03.css similarity index 100% rename from docs/examples/introduction/stopwatch03.css rename to docs/examples/tutorial/stopwatch03.css diff --git a/docs/examples/introduction/stopwatch03.py b/docs/examples/tutorial/stopwatch03.py similarity index 100% rename from docs/examples/introduction/stopwatch03.py rename to docs/examples/tutorial/stopwatch03.py diff --git a/docs/examples/introduction/stopwatch04.css b/docs/examples/tutorial/stopwatch04.css similarity index 100% rename from docs/examples/introduction/stopwatch04.css rename to docs/examples/tutorial/stopwatch04.css diff --git a/docs/examples/introduction/stopwatch04.py b/docs/examples/tutorial/stopwatch04.py similarity index 100% rename from docs/examples/introduction/stopwatch04.py rename to docs/examples/tutorial/stopwatch04.py diff --git a/docs/examples/introduction/stopwatch05.py b/docs/examples/tutorial/stopwatch05.py similarity index 100% rename from docs/examples/introduction/stopwatch05.py rename to docs/examples/tutorial/stopwatch05.py diff --git a/docs/examples/introduction/stopwatch06.py b/docs/examples/tutorial/stopwatch06.py similarity index 100% rename from docs/examples/introduction/stopwatch06.py rename to docs/examples/tutorial/stopwatch06.py diff --git a/docs/guide/CSS.md b/docs/guide/CSS.md index a65b24b00..684f7d372 100644 --- a/docs/guide/CSS.md +++ b/docs/guide/CSS.md @@ -10,7 +10,7 @@ CSS stands for _Cascading Stylesheets_. A stylesheet is a list of styles and rul Depending on what you want to build with Textual, you may not need to learn Textual CSS at all. Widgets are packaged with CSS styles so apps with exclusively pre-built widgets may not need any additional CSS. -Textual CSS defines a set of rules which apply visual _styles_ to your application and widgets. These style can customize a large variety of visual settings, such as color, border, size, alignment; and more dynamic features such as animation and hover effects. As powerful as it is, CSS in Textual is quite straightforward. +Textual CSS defines a set of rules which apply visual _styles_ to your application and widgets. These style can customize settings for properties such as color, border, size, alignment; and more dynamic features such as animation and hover effects. As powerful as it is, CSS in Textual is quite straightforward. CSS is typically stored in an external file with the extension `.css` alongside your Python code. @@ -30,7 +30,7 @@ This is an example of a CSS _rule set_. There may be many such sections in any g Let's break this CSS code down a bit. -```css hl_lines="1" +```sass hl_lines="1" Header { dock: top; height: 3; @@ -42,7 +42,7 @@ Header { The first line is a _selector_ which tells Textual which Widget(s) to modify. In the above example, the styles will be applied to a widget defined by the Python class `Header`. -```css hl_lines="2 3 4 5 6" +```sass hl_lines="2 3 4 5 6" Header { dock: top; height: 3; @@ -58,7 +58,7 @@ The first rule in the above example reads `"dock: top;"`. The rule name is `dock ## The DOM -The DOM, or _Document Object Model_, is a term borrowed from the web world. Textual doesn't use documents but the term has stuck. In Textual CSS, the DOM is a an arrangement of widgets you can visualize as a tree-like structure. +The DOM, or _Document Object Model_, is a term borrowed from the web world. Textual doesn't use documents but the term has stuck. In Textual CSS, the DOM is an arrangement of widgets you can visualize as a tree-like structure. Some widgets contain other widgets: for instance, a list control widget will likely also have item widgets, or a dialog widget may contain button widgets. These _child_ widgets form the branches of the tree. diff --git a/docs/guide/actions.md b/docs/guide/actions.md new file mode 100644 index 000000000..1060a658b --- /dev/null +++ b/docs/guide/actions.md @@ -0,0 +1 @@ +# Actions diff --git a/docs/guide/app.md b/docs/guide/app.md index 35aff27c0..70ed8f12f 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -1,6 +1,6 @@ # App Basics -In this chapter we will cover what you will need to know to build a Textual application. Just enough to get you up to speed. We will go in to more detail in the following chapters. +In this chapter we will cover how to use Textual's App class to create an application. Just enough to get you up to speed. We will go in to more detail in the following chapters. ## The App class @@ -26,20 +26,24 @@ Apps don't get much simpler than this—don't expect it to do much. If we run this app with `python simple02.py` you will see a blank terminal, something like the following: -```{.textual path="docs/examples/app/simple02.py" title="simple02.py"} +```{.textual path="docs/examples/app/simple02.py"} ``` -When you call [App.run()][textual.app.App.run] Textual puts the terminal in to a special state called *application mode*. When in application mode, the terminal will no longer echo what you type. Textual will take over responding to user input (keyboard and mouse) and will update the visible portion of the terminal (i.e. the *screen*). +When you call [App.run()][textual.app.App.run] Textual puts the terminal in to a special state called *application mode*. When in application mode the terminal will no longer echo what you type. Textual will take over responding to user input (keyboard and mouse) and will update the visible portion of the terminal (i.e. the *screen*). If you hit ++ctrl+c++ Textual will exit application mode and return you to the command prompt. Any content you had in the terminal prior to application mode will be restored. ## Events -Textual has an event system you can use to respond to key presses, mouse actions, and also internal state changes. Event handlers are methods which are prefixed with `on_` followed by the name of the event. +Textual has an event system you can use to respond to key presses, mouse actions, and internal state changes. Event handlers are methods which are prefixed with `on_` followed by the name of the event. One such event is the *mount* event which is sent to an application after it enters application mode. You can respond to this event by defining a method called `on_mount`. -Another such event is `on_key` which is sent when the user presses a key. The following example contains handlers for both those events: +!!! info + + You may have noticed we use the term "send" and "sent" in relation to event handler methods in preference to "calling". This is because Textual uses a message passing system where events are passed (or *sent*) between components. We will cover the details in [events][events.md]. + +Another such event is the *key* event which is sent when the user presses a key. The following example contains handlers for both those events: ```python title="event01.py" --8<-- "docs/examples/app/event01.py" @@ -50,21 +54,21 @@ The `on_mount` handler sets the `self.styles.background` attribute to `"darkblue ```{.textual path="docs/examples/app/event01.py" hl_lines="23-25"} ``` -The key event handler (`on_key`) specifies an `event` parameter which should be a [events.Key][textual.events.Key] instance. Every event has an associated event object which will be passed to the handler method if it is present in the method's parameter list. +The key event handler (`on_key`) specifies an `event` parameter which will receive a [events.Key][textual.events.Key] instance. Every event has an associated event object which will be passed to the handler method if it is present in the method's parameter list. !!! note - It is unusual for a method's parameters to affect how it is called. Textual accomplishes this by inspecting the method prior to calling it. + It is unusual (but not unprecedented) for a method's parameters to affect how it is called. Textual accomplishes this by inspecting the method prior to calling it. -For some events, such as the key event, there is additional information on the event object. In the case of [events.Key][textual.events.Key] it will contain the key that was pressed. +For some events, such as the key event, the event object contains additional information. In the case of [events.Key][textual.events.Key] it will contain the key that was pressed. -The `on_key` method above uses the `key` attribute on the Key event to change the background color if any of the keys 0-9 are pressed. +The `on_key` method above uses the `key` attribute on the Key event to change the background color if any of the keys ++0++ to ++9++ are pressed. ### Async events Textual is powered by Python's [asyncio](https://docs.python.org/3/library/asyncio.html) framework which uses the `async` and `await` keywords to coordinate events. -Textual knows to *await* your event handlers if they are generators (i.e. prefixed with the `async` keywords). +Textual knows to *await* your event handlers if they are generators (i.e. prefixed with the `async` keyword). !!! note @@ -88,10 +92,10 @@ The following example imports a builtin Welcome widget and yields it from compos When you run this code, Textual will *mount* the Welcome widget which contains a Markdown content area and a button: -```{.textual path="docs/examples/app/widgets01.py" title="widgets01.py" } +```{.textual path="docs/examples/app/widgets01.py"} ``` -Notice the `on_button_pressed` method which handles the [Button.Pressed][textual.widgets.Button] event send by the button contained in the Welcome widget. The handlers calls [App.exit()][textual.app.App] to exit the app. +Notice the `on_button_pressed` method which handles the [Button.Pressed][textual.widgets.Button] event sent by a button contained in the Welcome widget. The handler calls [App.exit()][textual.app.App] to exit the app. ### Mounting @@ -105,7 +109,7 @@ Here's an app which adds the welcome widget in response to any key press: When you first run this you will get a blank screen. Press any key to add the welcome widget. You can even press a key multiple times to add several widgets. -```{.textual path="docs/examples/app/widgets02.py" title="widgets02.py" press="a,a,a,down,down,down,down,down,down,_,_,_,_,_,_"} +```{.textual path="docs/examples/app/widgets02.py" press="a,a,a,down,down,down,down,down,down,_,_,_,_,_,_"} ``` ### Exiting @@ -138,3 +142,36 @@ The addition of `[str]` tells Mypy that `run()` is expected to return a string. !!! note Type annotations are entirely optional (but recommended) with Textual. + +## CSS + +Textual apps can reference [CSS](CSS.md) files which define how your app and widgets will look, while keeping your Python code free of display related code (which tends to be messy). + +The following chapter on [Textual CSS](CSS.md) will describe how to use CSS in detail. For now lets look at how your app references external CSS files. + +The following example sets the `css_path` attribute on the app: + +```python title="question02.py" hl_lines="15" +--8<-- "docs/examples/app/question02.py" +``` + +If the path is relative (as it is above) then it is taken as relative to where the app is defined. Hence this example references `"question01.css"` in the same directory as the Python code. Here is that CSS file: + +```sass title="question02.css" +--8<-- "docs/examples/app/question02.css" +``` + +When `"question02.py"` runs it will load `"question02.css"` and update the app and widgets accordingly. Even though the code is almost identical to the previous sample, the app now looks quite different: + +```{.textual path="docs/examples/app/question02.py"} +``` + +### Classvar CSS + +While external CSS files are recommended for most applications, and enable some cool features like *live editing* (see below), you can also specify the CSS directly within the Python code. To do this you can set the `CSS` class variable on the app which contains the CSS content. + +Here's the question app with classvar CSS: + +```python title="question03.py" hl_lines="6-24" +--8<-- "docs/examples/app/question03.py" +``` diff --git a/docs/guide/devtools.md b/docs/guide/devtools.md index a8ed972c1..140554268 100644 --- a/docs/guide/devtools.md +++ b/docs/guide/devtools.md @@ -27,7 +27,7 @@ textual run my_app.py The `run` sub-command assumes you have an App instance called `app` in the global scope of your Python file. If the application is called something different, you can specify it with a colon following the filename: -``` +```bash textual run my_app.py:alternative_app ``` @@ -35,9 +35,22 @@ textual run my_app.py:alternative_app If the Python file contains a call to app.run() then you can launch the file as you normally would any other Python program. Running your app via `textual run` will give you access to a few Textual features such as live editing of CSS files. + +## Live editing + +If you combine the `run` command with the `--dev` switch your app will run in *development mode*. + +```bash +textual run --dev my_app.py +``` + +One of the the features of *dev* mode is live editing of CSS files: any changes to your CSS will be reflected in the terminal a few milliseconds later. + +This is a great feature for iterating on your app's look and feel. Open the CSS in your editor and have your app running in a terminal. Edits to your CSS will appear almost immediately after you save. + ## Console -When running a terminal application, you will generally no longer be able to use `print` when debugging (or log to the console). This is because anything you write to standard output would overwrite application content, making it unreadable. Fortunately Textual supplies a debug console of its own which has some super helpful features. +When building a typical terminal application you are generally unable to use `print` when debugging (or log to the console). This is because anything you write to standard output will overwrite application content. Textual has a solution to this in the form of a debug console which restores `print` and adds a few additional features to help you debug. To use the console, open up **two** terminal emulators. Run the following in one of the terminals: @@ -58,6 +71,7 @@ textual run --dev my_app.py Anything you `print` from your application will be displayed in the console window. Textual will also write log messages to this window which may be helpful when debugging your application. + ### Verbosity Textual writes log messages to inform you about certain events, such as when the user presses a key or clicks on the terminal. To avoid swamping you with too much information, some events are marked as "verbose" and will be excluded from the logs. If you want to see these log messages, you can add the `-v` switch. @@ -91,7 +105,7 @@ log("[bold red]DANGER![/] We're having too much fun") ### Log method -There's a convenient shortcut to `log` available on the App and Widget objects you can use in event handlers: +There's a convenient shortcut to `log` available on the App and Widget objects. This is useful in event handlers. Here's an example: ```python from textual.app import App diff --git a/docs/guide/reactivity.md b/docs/guide/reactivity.md new file mode 100644 index 000000000..5a26ca9fb --- /dev/null +++ b/docs/guide/reactivity.md @@ -0,0 +1 @@ +# Reactivity diff --git a/docs/guide/screens.md b/docs/guide/screens.md new file mode 100644 index 000000000..5881073ec --- /dev/null +++ b/docs/guide/screens.md @@ -0,0 +1 @@ +# Screens diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md new file mode 100644 index 000000000..097bbeec9 --- /dev/null +++ b/docs/guide/widgets.md @@ -0,0 +1 @@ +# Widgets diff --git a/docs/introduction.md b/docs/tutorial.md similarity index 84% rename from docs/introduction.md rename to docs/tutorial.md index 062bb1958..6285cda81 100644 --- a/docs/introduction.md +++ b/docs/tutorial.md @@ -1,26 +1,26 @@ -# Introduction +# Tutorial -Welcome to the Textual Introduction! +Welcome to the Textual Tutorial! -By the end of this page you should have a solid understanding of app development with Textual. +By the end of this page you should have a solid understanding of app development with Textual. !!! quote - This page goes in to more detail than you may expect from an introduction. I like documentation to have complete working examples and I wanted the first app to be realistic. + I've always thought the secret sauce in making a popular framework is for it to be fun. I hope you enjoy this tutorial! — **Will McGugan** (creator of Rich and Textual) ## Stopwatch Application -We're going to build a stopwatch application. It should show a list of stopwatches with a time display the user can start, stop, and reset. We also want the user to be able to add and remove stopwatches as required. +We're going to build a stopwatch application. This application should show a list of stopwatches with a time display the user can start, stop, and reset. We also want the user to be able to add and remove stopwatches as required. This will be a simple yet **fully featured** app — you could distribute this app if you wanted to! Here's what the finished app will look like: -```{.textual path="docs/examples/introduction/stopwatch.py" press="tab,enter,_,tab,enter,_,tab,_,enter,_,tab,enter,_,_"} +```{.textual path="docs/examples/tutorial/stopwatch.py" press="tab,enter,_,tab,enter,_,tab,_,enter,_,tab,enter,_,_"} ``` ### Get the code @@ -45,10 +45,10 @@ If you want to try the finished Stopwatch app and follow along with the code, fi gh repo clone Textualize/textual ``` -With the repository cloned, navigate to `docs/examples/introduction` and run `stopwatch.py`. +With the repository cloned, navigate to `docs/examples/tutorial` and run `stopwatch.py`. ```bash -cd textual/docs/examples/introduction +cd textual/docs/examples/tutorial python stopwatch.py ``` @@ -58,7 +58,7 @@ python stopwatch.py Type hints are entirely optional in Textual. We've included them in the example code but it's up to you whether you add them to your own projects. -We're a big fan of Python type hints at Textualize. If you haven't encountered type hinting, it's a way to express the types of your data, parameters, and return values. Type hinting allows tools like [Mypy](https://mypy.readthedocs.io/en/stable/) to catch potential bugs before your code runs. +We're a big fan of Python type hints at Textualize. If you haven't encountered type hinting, it's a way to express the types of your data, parameters, and return values. Type hinting allows tools like [Mypy](https://mypy.readthedocs.io/en/stable/) to catch bugs before your code runs. The following function contains type hints: @@ -77,18 +77,18 @@ def repeat(text: str, count: int) -> str: The first step in building a Textual app is to import and extend the `App` class. Here's our basic app class with a few methods we will cover below. ```python title="stopwatch01.py" ---8<-- "docs/examples/introduction/stopwatch01.py" +--8<-- "docs/examples/tutorial/stopwatch01.py" ``` If you run this code, you should see something like the following: -```{.textual path="docs/examples/introduction/stopwatch01.py"} +```{.textual path="docs/examples/tutorial/stopwatch01.py"} ``` Hit the ++d++ key to toggle dark mode. -```{.textual path="docs/examples/introduction/stopwatch01.py" press="d" title="TimerApp + dark"} +```{.textual path="docs/examples/tutorial/stopwatch01.py" press="d" title="TimerApp + dark"} ``` Hit ++ctrl+c++ to exit the app and return to the command prompt. @@ -98,27 +98,27 @@ Hit ++ctrl+c++ to exit the app and return to the command prompt. Let's examine stopwatch01.py in more detail. ```python title="stopwatch01.py" hl_lines="1 2" ---8<-- "docs/examples/introduction/stopwatch01.py" +--8<-- "docs/examples/tutorial/stopwatch01.py" ``` The first line imports the Textual `App` class. The second line imports two builtin widgets: `Footer` which shows available keys and `Header` which shows a title and the current time. -Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build such widgets in this introduction. +Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build such widgets in this tutorial. ```python title="stopwatch01.py" hl_lines="5-19" ---8<-- "docs/examples/introduction/stopwatch01.py" +--8<-- "docs/examples/tutorial/stopwatch01.py" ``` The App class is where most of the logic of Textual apps is written. It is responsible for loading configuration, setting up widgets, handling keys, and more. Currently, there are three methods in our stopwatch app. -- **`compose()`** is where we construct a user interface with widgets. The `compose()` method may return a list of widgets, but it is generally easier to _yield_ them (making this method a generator). In the example code we yield instances of the widget classes we imported, i.e. the header and the footer. +- `compose()` is where we construct a user interface with widgets. The `compose()` method may return a list of widgets, but it is generally easier to _yield_ them (making this method a generator). In the example code we yield instances of the widget classes we imported, i.e. the header and the footer. -- **`on_load()`** is an _event handler_ method. Event handlers are called by Textual in response to external events like keys and mouse movements, and internal events needed to manage your application. Event handler methods begin with `on_` followed by the name of the event (in lower case). Hence, `on_load` is called in response to the Load event which is sent just after the app starts. We're using this event to call `App.bind()` which connects a key to an _action_. +- `on_load()` is an _event handler_ method. Event handlers are called by Textual in response to external events like keys and mouse movements, and internal events needed to manage your application. Event handler methods begin with `on_` followed by the name of the event (in lower case). Hence, `on_load` is called in response to the Load event which is sent just after the app starts. We're using this event to call `App.bind()` which connects a key to an _action_. -- **`action_toggle_dark()`** defines an _action_ method. Actions are methods beginning with `action_` followed by the name of the action. The call to `bind()` in `on_load()` binds this the ++d++ key to this action. The body of this method flips the state of the `dark` Boolean to toggle dark mode. +- `action_toggle_dark()` defines an _action_ method. Actions are methods beginning with `action_` followed by the name of the action. The call to `bind()` in `on_load()` binds this the ++d++ key to this action. The body of this method flips the state of the `dark` Boolean to toggle dark mode. !!! note @@ -126,7 +126,7 @@ Currently, there are three methods in our stopwatch app. ```python title="stopwatch01.py" hl_lines="22-24" ---8<-- "docs/examples/introduction/stopwatch01.py" +--8<-- "docs/examples/tutorial/stopwatch01.py" ``` The last few lines create an instance of the app at the module scope. Followed by a call to `run()` within a `__name__ == "__main__"` block. This is so that we could import `app` if we want to. Or we could run it with `python stopwatch01.py`. @@ -153,7 +153,7 @@ Textual has a builtin `Button` widget which takes care of the first three compon Let's add those to the app. Just a skeleton for now, we will add the rest of the features as we go. ```python title="stopwatch02.py" hl_lines="3 6-7 10-18 28" ---8<-- "docs/examples/introduction/stopwatch02.py" +--8<-- "docs/examples/tutorial/stopwatch02.py" ``` ### Extending widget classes @@ -180,7 +180,7 @@ The new line in `Stopwatch.compose()` yields a single `Container` object which w Let's see what happens when we run "stopwatch02.py". -```{.textual path="docs/examples/introduction/stopwatch02.py" title="stopwatch02.py"} +```{.textual path="docs/examples/tutorial/stopwatch02.py" title="stopwatch02.py"} ``` The elements of the stopwatch application are there. The buttons are clickable and you can scroll the container but it doesn't look like the sketch. This is because we have yet to apply any _styles_ to our new widgets. @@ -204,18 +204,18 @@ While it's possible to set all styles for an app this way, it is rarely necessar Let's add a CSS file to our application. ```python title="stopwatch03.py" hl_lines="39" ---8<-- "docs/examples/introduction/stopwatch03.py" +--8<-- "docs/examples/tutorial/stopwatch03.py" ``` Adding the `css_path` attribute to the app constructor tells Textual to load the following file when it starts the app: ```sass title="stopwatch03.css" ---8<-- "docs/examples/introduction/stopwatch03.css" +--8<-- "docs/examples/tutorial/stopwatch03.css" ``` If we run the app now, it will look *very* different. -```{.textual path="docs/examples/introduction/stopwatch03.py" title="stopwatch03.py"} +```{.textual path="docs/examples/tutorial/stopwatch03.py" title="stopwatch03.py"} ``` This app looks much more like our sketch. Textual has read style information from `stopwatch03.css` and applied it to the widgets. @@ -295,7 +295,7 @@ We can accomplish this with a CSS _class_. Not to be confused with a Python clas Here's the new CSS: ```sass title="stopwatch04.css" hl_lines="33-53" ---8<-- "docs/examples/introduction/stopwatch04.css" +--8<-- "docs/examples/tutorial/stopwatch04.css" ``` These new rules are prefixed with `.started`. The `.` indicates that `.started` refers to a CSS class called "started". The new styles will be applied only to widgets that have this CSS class. @@ -323,14 +323,14 @@ You can add and remove CSS classes with the `add_class()` and `remove_class()` m The following code adds an event handler for the `Button.Pressed` event. ```python title="stopwatch04.py" hl_lines="13-18" ---8<-- "docs/examples/introduction/stopwatch04.py" +--8<-- "docs/examples/tutorial/stopwatch04.py" ``` The `on_button_pressed` event handler is called when the user clicks a button. This method adds the "started" class when the "start" button was clicked, and removes the class when the "stop" button is clicked. If you run "stopwatch04.py" now you will be able to toggle between the two states by clicking the first button: -```{.textual path="docs/examples/introduction/stopwatch04.py" title="stopwatch04.py" press="tab,tab,tab,enter"} +```{.textual path="docs/examples/tutorial/stopwatch04.py" title="stopwatch04.py" press="tab,tab,tab,enter"} ``` ## Reactive attributes @@ -340,7 +340,7 @@ A recurring theme in Textual is that you rarely need to explicitly update a widg You can declare a reactive attribute with `textual.reactive.Reactive`. Let's use this feature to create a timer that displays elapsed time and keeps it updated. ```python title="stopwatch04.py" hl_lines="1 5 12-27" ---8<-- "docs/examples/introduction/stopwatch05.py" +--8<-- "docs/examples/tutorial/stopwatch05.py" ``` We have added two reactive attributes: `start_time` will contain the time in seconds when the stopwatch was started, and `time` will contain the time to be displayed on the Stopwatch. @@ -368,7 +368,7 @@ Because `watch_time` watches the `time` attribute, when we update `self.time` 60 The end result is that the `Stopwatch` widgets show the time elapsed since the widget was created: -```{.textual path="docs/examples/introduction/stopwatch05.py" title="stopwatch05.py"} +```{.textual path="docs/examples/tutorial/stopwatch05.py" title="stopwatch05.py"} ``` We've seen how we can update widgets with a timer, but we still need to wire up the buttons so we can operate Stopwatches independently. @@ -379,7 +379,7 @@ We need to be able to start, stop, and reset each stopwatch independently. We ca ```python title="stopwatch06.py" hl_lines="14-44 50-61" ---8<-- "docs/examples/introduction/stopwatch06.py" +--8<-- "docs/examples/tutorial/stopwatch06.py" ``` Here's a summary of the changes made to `TimeDisplay`. @@ -415,7 +415,7 @@ This code supplies missing features and makes our app useful. We've made the fol If you run stopwatch06.py you will be able to use the stopwatches independently. -```{.textual path="docs/examples/introduction/stopwatch06.py" title="stopwatch06.py" press="tab,enter,_,_,tab,enter,_,tab"} +```{.textual path="docs/examples/tutorial/stopwatch06.py" title="stopwatch06.py" press="tab,enter,_,_,tab,enter,_,tab"} ``` The only remaining feature of the Stopwatch app left to implement is the ability to add and remove timers. @@ -429,7 +429,7 @@ To add a new child widget call `mount()` on the parent. To remove a widget, call Let's use these to implement adding and removing stopwatches to our app. ```python title="stopwatch.py" hl_lines="83-84 86-90 92-96" ---8<-- "docs/examples/introduction/stopwatch.py" +--8<-- "docs/examples/tutorial/stopwatch.py" ``` We've added two new actions: `action_add_stopwatch` to add a new stopwatch, and `action_remove_stopwatch` to remove the last stopwatch. The `on_load` handler binds these actions to the ++a++ and ++r++ keys. @@ -440,11 +440,11 @@ The `action_remove_stopwatch` calls `query` with a CSS selector of `"Stopwatch"` If you run `stopwatch.py` now you can add a new stopwatch with the ++a++ key and remove a stopwatch with ++r++. -```{.textual path="docs/examples/introduction/stopwatch.py" press="d,a,a,a,a,a,a,a,tab,enter,_,_,_,_,tab,_"} +```{.textual path="docs/examples/tutorial/stopwatch.py" press="d,a,a,a,a,a,a,a,tab,enter,_,_,_,_,tab,_"} ``` ## What next? -Congratulations on building your first Textual application! This introduction has covered a lot of ground. If you are the type that prefers to learn a framework by coding, feel free. You could tweak stopwatch.py or look through the examples. +Congratulations on building your first Textual application! This tutorial has covered a lot of ground. If you are the type that prefers to learn a framework by coding, feel free. You could tweak stopwatch.py or look through the examples. Read the guide for the full details on how to build sophisticated TUI applications with Textual. diff --git a/mkdocs.yml b/mkdocs.yml index 7b413fd24..c7880436b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,13 +4,17 @@ site_url: https://www.textualize.io/ nav: - "index.md" - "getting_started.md" - - "introduction.md" + - "tutorial.md" - Guide: - - "guide/app.md" - "guide/devtools.md" - - "guide/layout.md" + - "guide/app.md" - "guide/CSS.md" + - "guide/layout.md" - "guide/events.md" + - "guide/actions.md" + - "guide/reactivity.md" + - "guide/widgets.md" + - "guide/screens.md" - How to: - "how-to/animation.md" - "how-to/mouse-and-keyboard.md" diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index d2088cfc9..3b406be1b 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -45,7 +45,8 @@ class Button(Widget, can_focus=True): } Button.-disabled { - opacity: 0.6; + opacity: 0.4; + text-opacity: 0.7; } Button:focus { @@ -84,7 +85,6 @@ class Button(Widget, can_focus=True): background: $primary; border-bottom: tall $primary-lighten-3; border-top: tall $primary-darken-3; - } @@ -94,13 +94,11 @@ class Button(Widget, can_focus=True): color: $text-success; border-top: tall $success-lighten-2; border-bottom: tall $success-darken-3; - } Button.-success:hover { background: $success-darken-2; color: $text-success-darken-2; - } Button.-success.-active { @@ -199,6 +197,11 @@ class Button(Widget, can_focus=True): variant = Reactive.init("default") disabled = Reactive(False) + def watch_mouse_over(self, value: bool) -> None: + """Update from CSS if mouse over state changes.""" + if not self.disabled: + self.app.update_styles(self) + def validate_variant(self, variant: str) -> str: if variant not in _VALID_BUTTON_VARIANTS: raise InvalidButtonVariant( From 195bb310440c17589d36889192c39d2536d8627a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 5 Sep 2022 21:35:32 +0100 Subject: [PATCH 07/24] tweak --- docs/tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 6285cda81..abb70ac6f 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -6,7 +6,7 @@ By the end of this page you should have a solid understanding of app development !!! quote - I've always thought the secret sauce in making a popular framework is for it to be fun. I hope you enjoy this tutorial! + I've always thought the secret sauce in making a popular framework is for it to be fun. — **Will McGugan** (creator of Rich and Textual) From d301066d8c05a49878bd56f7713f2c4b106a0b55 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 5 Sep 2022 21:41:45 +0100 Subject: [PATCH 08/24] fix test error --- src/textual/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/app.py b/src/textual/app.py index 8ba711ddf..62acf53b5 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -98,7 +98,7 @@ DEFAULT_COLORS = { } -ComposeResult = Iterable[Widget] | Generator[Widget, Widget, None] +ComposeResult = "Iterable[Widget] | Generator[Widget, Widget, None]" class AppError(Exception): From 6d6e385313efbf40b717c2340af6c1a639723553 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 5 Sep 2022 22:58:15 +0100 Subject: [PATCH 09/24] added styles --- docs/guide/styles.md | 3 +++ mkdocs.yml | 1 + 2 files changed, 4 insertions(+) create mode 100644 docs/guide/styles.md diff --git a/docs/guide/styles.md b/docs/guide/styles.md new file mode 100644 index 000000000..5f968b8aa --- /dev/null +++ b/docs/guide/styles.md @@ -0,0 +1,3 @@ +# Styles + +TODO: discussion of box model diff --git a/mkdocs.yml b/mkdocs.yml index c7880436b..2bbbb0c94 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,6 +9,7 @@ nav: - "guide/devtools.md" - "guide/app.md" - "guide/CSS.md" + - "guide/styles.md" - "guide/layout.md" - "guide/events.md" - "guide/actions.md" From e8a4f2e806ef18d07849d1c98e0a0d16effbacab Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 10:16:52 +0100 Subject: [PATCH 10/24] rename table to gird, diagrams for layout --- docs/examples/app/question02.css | 6 +-- docs/guide/layout.md | 48 ++++++++++++----- .../align.excalidraw.svg} | 0 .../center.excalidraw.svg} | 0 docs/images/layout/dock.excalidraw.svg | 16 ++++++ .../grid.excalidraw.svg} | 0 .../horizontal.excalidraw.svg} | 0 docs/images/layout/offset.excalidraw.svg | 16 ++++++ .../vertical.excalidraw.svg} | 0 examples/calculator.css | 10 ++-- sandbox/will/calculator.css | 10 ++-- sandbox/will/table_layout.css | 8 +-- src/textual/css/_styles_builder.py | 34 ++++++------ src/textual/css/constants.py | 2 +- src/textual/css/styles.py | 52 +++++++++---------- src/textual/layouts/factory.py | 4 +- src/textual/layouts/{table.py => grid.py} | 18 +++---- 17 files changed, 138 insertions(+), 86 deletions(-) rename docs/images/{layout_align.excalidraw.svg => layout/align.excalidraw.svg} (100%) rename docs/images/{layout_center.excalidraw.svg => layout/center.excalidraw.svg} (100%) create mode 100644 docs/images/layout/dock.excalidraw.svg rename docs/images/{layout_table.excalidraw.svg => layout/grid.excalidraw.svg} (100%) rename docs/images/{layout_horizontal.excalidraw.svg => layout/horizontal.excalidraw.svg} (100%) create mode 100644 docs/images/layout/offset.excalidraw.svg rename docs/images/{layout_vertical.excalidraw.svg => layout/vertical.excalidraw.svg} (100%) rename src/textual/layouts/{table.py => grid.py} (91%) diff --git a/docs/examples/app/question02.css b/docs/examples/app/question02.css index 8145fdd72..1f1a3b84b 100644 --- a/docs/examples/app/question02.css +++ b/docs/examples/app/question02.css @@ -1,7 +1,7 @@ Screen { - layout: table; - table-size: 2; - table-gutter: 2; + layout: grid; + grid-size: 2; + grid-gutter: 2; padding: 2; } #question { diff --git a/docs/guide/layout.md b/docs/guide/layout.md index 6f10cf228..c04fea024 100644 --- a/docs/guide/layout.md +++ b/docs/guide/layout.md @@ -1,56 +1,76 @@ # Layout -TODO: Explanation of layout +In textual the *layout* defines how widgets will be arranged (or *layed out*) on the screen. Textual supports a number of layouts which can be set either via a widgets `styles` object or via CSS. -## Vertical layout +## Vertical + +A vertical layout will place new widgets below previous widgets, starting from the top of the screen.
---8<-- "docs/images/layout_vertical.excalidraw.svg" +--8<-- "docs/images/layout/vertical.excalidraw.svg"
TODO: Explanation of vertical layout -## Horizontal layout +## Horizontal + +A horizontal layout will place the first widget at the top left of the screen, and new widgets will be place directly to the right of the previous widget.
---8<-- "docs/images/layout_horizontal.excalidraw.svg" +--8<-- "docs/images/layout/horizontal.excalidraw.svg"
TODO: Explantion of horizontal layout -## Center layout +## Center + +A center widget will place the widget directly in the center of the screen. New widgets will also be placed in the center of the screen, overlapping previous widgets. + +There probably isn't a practical use for such overlapping widgets. In practice this layout is probably only useful where you have a single child widget.
---8<-- "docs/images/layout_center.excalidraw.svg" +--8<-- "docs/images/layout/center.excalidraw.svg"
TODO: Explanation of center layout -## Table layout +## Grid +A grid layout arranges widgets within a grid composed of columns and rows. Widgets can span multiple rows or columns to create more complex layouts.
---8<-- "docs/images/layout_table.excalidraw.svg" +--8<-- "docs/images/layout/grid.excalidraw.svg"
-TODO: Explanation of table layout +TODO: Explanation of grid layout -## Dock +## Docking + +Widgets may be *docked*. Docking a widget removes it from the layout and fixes it position, aligned to either the top, right, bottom, or left edges of the screen. Docked widgets will not scroll, making them ideal for fixed headers / footers / sidebars. + +
+--8<-- "docs/images/layout/dock.excalidraw.svg" +
+ TODO: Diagram TODO: Explanation of dock ## Offsets +Widgets have a relative offset which is added to the widget's location, after its location has been determined via its layout. + +
+--8<-- "docs/images/layout/offset.excalidraw.svg" +
+ + TODO: Diagram TODO: Offsets -## Box Model - -TBC diff --git a/docs/images/layout_align.excalidraw.svg b/docs/images/layout/align.excalidraw.svg similarity index 100% rename from docs/images/layout_align.excalidraw.svg rename to docs/images/layout/align.excalidraw.svg diff --git a/docs/images/layout_center.excalidraw.svg b/docs/images/layout/center.excalidraw.svg similarity index 100% rename from docs/images/layout_center.excalidraw.svg rename to docs/images/layout/center.excalidraw.svg diff --git a/docs/images/layout/dock.excalidraw.svg b/docs/images/layout/dock.excalidraw.svg new file mode 100644 index 000000000..fde040991 --- /dev/null +++ b/docs/images/layout/dock.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2aWXPbNlx1MDAxMIDf8ys0ymvM4D4y0+nItpKocWwnjuOj0+nQJCTSokiWpCxLXHUwMDE5//cuaVXURVx1MDAxZrKqqtGDLWFcdGBcdHy72Fx1MDAwNfDjVa1Wz4axqb+r1c2tY1x1MDAwN76b2IP6m7z8xiSpXHUwMDFmhSBcIsXvNOonTvGkl2Vx+u7t256ddE1cdTAwMTZcdTAwMDe2Y6xcdTAwMWI/7dtBmvVdP7KcqPfWz0wv/TX/e2j3zC9x1HOzxCo72TGun0XJfV8mMD1cdTAwMTNmKbT+O/yu1X5cdTAwMTR/p7RLjJPZYScwRYVCVCrIXHSbLz2MwkJZzDXHTCuMJk/46T70l1x1MDAxOVx1MDAxN8Rt0NmUkryovk9cdTAwMWKNLDiPm7vvveD4YtQ6b3mdstu2XHUwMDFmXHUwMDA0J9kwKNRykihNdzw7c7zyiTRLoq45893MyzWYK5/UTSNcdTAwMTiJslZcdTAwMTL1O15o0nSmTlx1MDAxNNuOn1xy8zJUvsL9SLyrlSW3+TBcYksppLimkk9cdTAwMDR5VVwiLc0wQ4LwOWX2olx1MDAwMOZcdTAwMDCUeY2KT6nOle10O6BT6E6eyVx1MDAxMjtMYzuBmSqfXHUwMDFijF+TaWFRqfRMJ57xO15cdTAwMDZSSpSl2LReqSkmQFLMXHUwMDE4RVxcTVx1MDAwNHmnccstWPhjfuw8O4nHY1RP81x1MDAxZlNcbue6NqdAKiv3Y9e+n28sXHUwMDA0YVx1MDAxYyOtJKJcdTAwMTN54IddXHUwMDEwhv0gKMtcIqdbXCJSlN69WYFNrGklm4wjXCI1k09nM+yfXHUwMDFj73qX3/rdXHUwMDEz3jvY/Vx1MDAxNCFf7lWwOcfXLJVkk1RKhlxiZUuoJJjNUbF2KplVgSRcdTAwMTFcdTAwMTYmQMIyKJHGXHUwMDFhXHUwMDFjXHUwMDA3+lx1MDAxZkNpgsCP0+VIXG5VhaQg4D84werJRLZUvG9uj6L9a9p2/mzEPlx1MDAxYuhwXHUwMDE1XCI35yeFtKhSQmI1RyRcdTAwMDVUheZcdTAwMTS/zE++btuccLJIIyaLxE94xNhic13f08hcdTAwMDVSQlx1MDAwYkV+Tlx1MDAxYVx0IVU0YiykJuA7no6jz0dnXel+uP14ddL63CW94Pw82G5cdTAwMWM1tjSli6s25ZbgL1xcslx1MDAwMcUrhPi6UCyCXGLNlPhJUZSiXG5FmFx1MDAxZKQwsPhkXHUwMDEym8R8O/R6XzzxvXf05f1+s5mNXHUwMDBltptEjKXF5Cx0Y1x1MDAxMl9cdTAwMWE7vsbkXG587rpA1FgqJqT8P6/QXHUwMDBmh41cdTAwMTC5VLpFSlx1MDAxOVx1MDAxM1x1MDAxMOc/PW68uTj8XHUwMDFj68ug2T/5yptcdTAwMTk7v4pdv1x1MDAwMkbPdrx+Yv57XHUwMDFjuVx1MDAwNjTIXFxKkVfldJ3rtFhGJdWWXHUwMDEwRcZ031x1MDAxMF2Ek1wiYtFZ5cZ0XG4miCZywys2jFx1MDAwNkViM3TKXHUwMDA3Mm4qXGJoJJ+R1Vx1MDAxY1x1MDAxZYlcdTAwMTZpXFz3XHUwMDAzX35cdTAwMTmq9vfWzaCRbjudXHUwMDA0c0stYJjXXHUwMDE11FKQ2NGXZjZjn7mMT8iYLYZcdTAwMTHmS1NcdTAwMWJJQCghZJRMXHUwMDE3n3lAMVx1MDAwNF2SK0o2SygkXHUwMDE3SMrNXHUwMDEwqpSsXCKU5IqoZ1x1MDAxMbpz9MGcfo/OzcDP1M7p8V+O23K2n1BlgUdcdTAwMTBswYFiziyBJCMzKdB6XHUwMDExJVpZfFVAXHUwMDA1olQwyjaLJ5jFxvBElVmPRlhTNZ1cdTAwMTY9Rif969PnVn/onO3tfrn0mmfNTvL5z+2nXHUwMDEzsu2lyzthzFKU6lx1MDAxN29ccj1Cp0RcdTAwMTDN4dVcdTAwMWMoQ+A/MaGbXHJAYc2hXHUwMDAyb4RQgmTlLlx1MDAxMURHVEJcdTAwMWH4jGyo8enyMOy6UUz6R3vu7c23bD+Mt1x1MDAxZFFOmSU5X7KjLoWl+Dr8JyHqyiwlXHUwMDE0wotcIrZcdTAwMThcdTAwMTOotVjkXHUwMDE0XHUwMDBibTGxdFx1MDAwZlx1MDAxM1x1MDAxMiRcblx1MDAxM1x1MDAwNPRveIlcdTAwMTeYboZQrFHl1rpSmmBKpnKox1x1MDAwMFx1MDAxNa33e8e/XHRcdTAwMTnstM2+STJFXHUwMDBm+61tXHUwMDA3NF/hXHRcdTAwMTWUqPlcdTAwMTiUQs4uOV5DkvSQXHUwMDBmXHUwMDA1K1x1MDAxOJ83XHUwMDE1XHItI1RbsJapXHUwMDEyYzpcdTAwMGYqpVx1MDAxNFDlerMnQDBykESvi1M7SaLBUi/KKlx1MDAxMWWUS4LwM1x1MDAwZSbbl1xyXHUwMDE571x1MDAwN5/E6FwiuKTy4243XHUwMDFjXHUwMDBlVkN0c8c/WECaRDBmRFx1MDAxMIWEwnKGU5afTT5cZil3NEPuqpDmm1xiXHUwMDEwXHUwMDAyc5jyonu2yChB2lr0n1x1MDAwMoPqXG6vcjJZKLdcdTAwMWGXlMKQPsd/TulhJ9muXHUwMDFmun7YXHUwMDAx4T+M1ian661cdTAwMDKig49fvUBcdTAwMGVcdGmedk4v5ECnX9HpRNdcdTAwMWOjyOnnWu4gXHUwMDBiwlxywlx1MDAxNbhcdTAwMTdCNSyDik891rHjYv4teZ/ojiV3XHUwMDEzfUzoltrMvoCdZntRr+dn8OrHkVx1MDAxZmbzT1x1MDAxNO/SyI3KM7Y7L4WWp2Xz1lx1MDAxN+ctlldcdPJP+a1W4ln8mHz/483Sp3cq+SmkXHUwMDA1OmVcdTAwMWKvpv9XOYvM3GZLfVx1MDAwNa5cZrgkg0BcdTAwMTRcdTAwMDLO0sE+5itcdTAwMWWe5i1dznCxnC1cdTAwMWNcdTAwMTQzXG6xOnn5WUi1k4BwfolbWPBcdFx1MDAxME9cdIhcdP+N449cdEM/pvB6kt+fYeveXHUwMDE0JpK7f4D8N1x1MDAxY047XG6zXHUwMDEzf1TsqKCZ0vd2z1x1MDAwZoYzXHUwMDE4XHUwMDE00OeXa4q2ajDwXHUwMDFkk01PV2qg61wiuVAzlVx1MDAxYYHfya2jXHUwMDFlmPas2WS+Y1x1MDAwN1x1MDAxM3FcdTAwMTZNXHKvXHUwMDAzStjQXFzSWnBcdTAwMWVR4nf80Fx1MDAwZb4tVWglw8XVW02QIVxuSVx1MDAxOFJPz5RcdTAwMGVcdTAwMDJzdINij35cdTAwMThdOFe/XVx1MDAwZkbBaMWt+s2t8lRLaz6NXHUwMDA3O7aUnj+/WfNcdTAwMDVcdTAwMGbMS4VcdTAwMWawXFyMXHUwMDE01lx1MDAxMHuwXHKb7uHxdYNeKXamlZdcXCeDXHUwMDBmo1x1MDAwMTtYm+lyjMRz9qteZrpcdTAwMDf2MOpnY0tJt8F25zRaMUTn1Vx1MDAxN7RcdTAwMTCh+DnXs1x1MDAxZZ7uLbVdxoVFXHUwMDE5yq/hIaXx1GWL+1x1MDAwMJ1Z+rG7g0q2+dXqRqykhbjGeqzA1MZTadJcdTAwMTBcdTAwMWFUXdeCcsGJ5qusyy9cdNVz81OrmN9TQ/VcdTAwMDdXgtlQXHUwMDFkVpj8SFxcUYihJFXlJJahOrG0QkyonzdWr+SokE4jVFx1MDAxNbK/XHUwMDFhN1634/gkg/meTFx1MDAwZiDlu2OfWb5h/cY3g91lR8vFJ3dJxSjnpm/y9/xx9+rub5B4Q/4ifQ== + + + + Docked widgetLayout widgets \ No newline at end of file diff --git a/docs/images/layout_table.excalidraw.svg b/docs/images/layout/grid.excalidraw.svg similarity index 100% rename from docs/images/layout_table.excalidraw.svg rename to docs/images/layout/grid.excalidraw.svg diff --git a/docs/images/layout_horizontal.excalidraw.svg b/docs/images/layout/horizontal.excalidraw.svg similarity index 100% rename from docs/images/layout_horizontal.excalidraw.svg rename to docs/images/layout/horizontal.excalidraw.svg diff --git a/docs/images/layout/offset.excalidraw.svg b/docs/images/layout/offset.excalidraw.svg new file mode 100644 index 000000000..7e56bed90 --- /dev/null +++ b/docs/images/layout/offset.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ZWVPbSFx1MDAxMIDf+Vx1MDAxNZTzXHUwMDFhK3NcdTAwMWap2triXlxiS8JcdTAwMTVcYlspSkhjWWtZUqQx2Enx37clXHUwMDFjS744XGZhXHR6sK3p0Uxr5utr/GNpeblhXHUwMDA3qWm8X26YvudGoZ+5V423RfulyfIwiUFEyvs86WVe2bNtbZq/f/eu62ZcdTAwMWRj08j1jHNcdTAwMTnmPTfKbc9cdTAwMGZcdTAwMTPHS7rvQmu6+Z/F557bNX+kSde3mVNN0jR+aJPsZi5cdTAwMTOZroltXHUwMDBlo/9cdTAwMDP3y8s/ys+adpnxrFx1MDAxYlx1MDAwN5EpXHUwMDFmKEWVgpywyda9JC6VJVxcaM1cdTAwMDRBo1x1MDAwZWG+XHUwMDBl01njg7RcdTAwMDUqm0pSNDU2/L/pRvvbv70g+1x1MDAxNlxmVulgTZ82q1lbYVx1MDAxNFx1MDAxZNpBVGrlZUmeN9uu9dpVj9xmScechL5tQ1x1MDAxZjzRPno2T2AhqqeypFx1MDAxN7Rjk+djzySp64V2ULSh6lx1MDAxNW5cdTAwMTbi/XLV0i96UO4ohVx1MDAxNNdU8pGkeJYw7miGXHUwMDE5XHUwMDEyhE+os5ZEsFx0oM5cdTAwMWJUXpVCXHUwMDE3rtdcdECr2Fx1MDAxZvWxmVx1MDAxYuepm8FWVf2uhi/KtHCoVHpskrZcdIO2XHUwMDA1KSXKUayuWG7KLSCIYKWkpnokKWZNt/2Shq+Ty9d2s3S4TI28uKlpXFwou1FDqXq4l/ruzZZjIWA5XGJcdTAwMTDBdbV+UVx1MDAxOHdAXHUwMDE496Koaku8TkVJ2Xr9dlx1MDAwMTqxpvPoxJxgyTGW5N54XHUwMDFl2Y39081wV7ubXHUwMDFiJjv1z9OrvXl4TiA2XHUwMDBlJnlWMCVDhLJZYFx1MDAxMswmwHhyMJkzh0pcIlx1MDAxY0ww0jO4xEpQyonC9Dfm0kRRmOazqVx1MDAxNGoulVJcdTAwMTJcdTAwMDFb8lx1MDAwMCqT1Ytm/7CXx53Vg72TY4hcdTAwMDWfcGdcdTAwMTEqn9FdMvBXSlx0idUklZw5UmhO8ePc5ZuWy1x0J9NEYjJN/YhJjFx1MDAxZDYx9VxykUIrXHUwMDBlNqTx61x1MDAwNJKQuW5SIcxcdTAwMDVcdTAwMTX8/jz2XHUwMDBljoONwzPvUH6x4V/euWKnweZcdTAwMGLnUVwiR4PLmY7enDqTcXUxXHUwMDFhL1x1MDAxMOJPRaNUQkjxat0jkWKue0RCYK5FrcedOaV111vrQaY656q/x21rv1x1MDAxYixcdTAwMTS0n1x1MDAxMUdI55hcdTAwMTS8Tt1PXHUwMDFhXHUwMDFmjVwiJlx1MDAxN+B5n1xuRXDgiEtBlfyNWbw9hYQ0cS6OTCHN1Vx1MDAwM2hcXNvaOuqibbZ1XHUwMDE2fVx1MDAxOFxmdnbyj0r259DYdr12LzP/P49Eclx1MDAwN3EhxouYkkilXHUwMDFjOonq4uFazOJSI4fIMn+9XHUwMDE5iE7jKYnDsNRCSabLa1xuU1akXHUwMDE0XHUwMDE0iV9Q6VxmXHUwMDA1XHUwMDE1V7Xt3jnZXHUwMDBm6cpcdTAwMGU/kGebO1v+Jv34feXbaKwxXGLdLEuuXHUwMDFhI8n18NdcdTAwMGIxXHUwMDAyRvlcXCNcdTAwMTBcdTAwMTLyOMJqSe1dVtBstdbTXcp2dz+fb1xyzk74v51IvXQroFx1MDAxYUNcdTAwMTlccu+JucRIsYlUgVx1MDAxMeJcYqxcdTAwMWZf6Vx1MDAwZj30bFugo8OEhW2BgsdiYFxmr8pcdTAwMTRcbv+An8dcdTAwMTSEnpueXHUwMDEwpLDWhUO8tylE6+GXo1x1MDAwM2mClt7vtN0zkXhb+Us3hVwiICguXHUwMDE5marfXHUwMDE4l1x1MDAwZSNS0sdcdTAwMWUr3Fx1MDAxNlx1MDAxMVx1MDAxNHNcdTAwMTRReGhcdTAwMDNcYolcdTAwMDWsXHUwMDAwXHUwMDA24Fx1MDAwNKL373z0dWMnM931/FxmmjHQXHUwMDAxq/s769tccvdBhD7fuVx1MDAxN9RcdTAwMTDFiShDgKnCUvFxTGmR0eC7jlx1MDAxOZRs8YvFXHUwMDBmv6BYccCPa4Uxhe3neFx1MDAxYdPicJgzpYnSklx1MDAwYkbVJKZQknIsXHUwMDE5W6DUKzVdXHUwMDEw08J8yVx1MDAwMzCt6eFmdjWM/TBcdTAwMGVAWMWBn/8zbN8jXHIuqEq8Xrn7XHUwMDBllpD4gVx1MDAxOeNcItxcdTAwMDG8tU6Bm5ZEO1BcdTAwMTiVNfpQdD1Sx8T+3crcno3UlGlcIlx1MDAwN4FLwVx1MDAwNPJdpTVcdTAwMDRRxKbUUY7kRCvGOIJccuVCTilcdTAwMTW5uV1Lut3QwqJ/SsLYTi5uuYorhXW3jetPSuGl6rJJN5BcdTAwMTYjjofj6tdyZSflzej317cze89luLim6K1GW6p/z/Nf1vTtLPfF0S3uiyNYf0iB7u2/Lq8+dE6PV7+vN9m2/Cy7l3ZggpdcdTAwMWVhsdLgv1x1MDAwNNNMICaAqGpFSv8llFx1MDAwM9ZQxC+tMUTbx4TaW51YLauvju6nj1x1MDAwMSTkpFJcdTAwMTDyzMdcdTAwMDBcdTAwMTTXI9lcdTAwMDP8VCuJ7WH4/SZpXHUwMDFia910u2E0XHUwMDE427iSU9D0Y6uVXHUwMDFiW1/L3MCcJZdqrPdKXHUwMDE0XHUwMDA2cZnemdY44jb03GgktkntzT2Y3YXhsu0pk0+yMFxiYzc6XHUwMDFh1+RcdTAwMTFZLNdkro1xjFx1MDAwNUVS3d/G9pvHXHUwMDFl2ewrlVE//JCGe8dcdTAwMWLu2Vx1MDAxM9uYn1x1MDAxNP7yaZNcdTAwMDTmcKRnnLRRSVx1MDAxZHDv4tf+bfsk5Vx1MDAxYyZcZnKEwlx1MDAxYV5TPVfkyVx1MDAwZq/nloaDNtw0PbQw5Cjqw5qE/tDgq2FcdTAwMWGXoblanVV8lFehcmldXHUwMDA1wKZYkVx1MDAxZtdL1/9cdTAwMDE0elVbIn0= + + + + Offset \ No newline at end of file diff --git a/docs/images/layout_vertical.excalidraw.svg b/docs/images/layout/vertical.excalidraw.svg similarity index 100% rename from docs/images/layout_vertical.excalidraw.svg rename to docs/images/layout/vertical.excalidraw.svg diff --git a/examples/calculator.css b/examples/calculator.css index 0d777edfa..4a1d15663 100644 --- a/examples/calculator.css +++ b/examples/calculator.css @@ -3,11 +3,11 @@ Screen { } #calculator { - layout: table; - table-size: 4; - table-gutter: 1 2; - table-columns: 1fr; - table-rows: 2fr 1fr 1fr 1fr 1fr 1fr; + layout: grid; + grid-size: 4; + grid-gutter: 1 2; + grid-columns: 1fr; + grid-rows: 2fr 1fr 1fr 1fr 1fr 1fr; margin: 1 2; min-height:25; min-width: 26; diff --git a/sandbox/will/calculator.css b/sandbox/will/calculator.css index 0d777edfa..4a1d15663 100644 --- a/sandbox/will/calculator.css +++ b/sandbox/will/calculator.css @@ -3,11 +3,11 @@ Screen { } #calculator { - layout: table; - table-size: 4; - table-gutter: 1 2; - table-columns: 1fr; - table-rows: 2fr 1fr 1fr 1fr 1fr 1fr; + layout: grid; + grid-size: 4; + grid-gutter: 1 2; + grid-columns: 1fr; + grid-rows: 2fr 1fr 1fr 1fr 1fr 1fr; margin: 1 2; min-height:25; min-width: 26; diff --git a/sandbox/will/table_layout.css b/sandbox/will/table_layout.css index a37f1c169..2b79bcbad 100644 --- a/sandbox/will/table_layout.css +++ b/sandbox/will/table_layout.css @@ -1,8 +1,8 @@ Screen { - layout: table; - table-columns: 2fr 1fr 1fr; - table-rows: 1fr 1fr; - table-gutter: 1 2; + layout: grid; + grid-columns: 2fr 1fr 1fr; + grid-rows: 1fr 1fr; + grid-gutter: 1 2; } Static { diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 2037897f7..5ac422579 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -847,7 +847,7 @@ class StylesBuilder: self.error(name, token, scrollbar_size_single_axis_help_text(name)) self.styles._rules["scrollbar_size_horizontal"] = value - def _process_table_rows_or_columns(self, name: str, tokens: list[Token]) -> None: + def _process_grid_rows_or_columns(self, name: str, tokens: list[Token]) -> None: scalars: list[Scalar] = [] for token in tokens: if token.name == "number": @@ -867,8 +867,8 @@ class StylesBuilder: ) self.styles._rules[name.replace("-", "_")] = scalars - process_table_rows = _process_table_rows_or_columns - process_table_columns = _process_table_rows_or_columns + process_grid_rows = _process_grid_rows_or_columns + process_grid_columns = _process_grid_rows_or_columns def _process_integer(self, name: str, tokens: list[Token]) -> None: if not tokens: @@ -884,14 +884,14 @@ class StylesBuilder: self.error(name, token, integer_help_text(name)) self.styles._rules[name.replace("-", "_")] = value - process_table_gutter_horizontal = _process_integer - process_table_gutter_vertical = _process_integer + process_grid_gutter_horizontal = _process_integer + process_grid_gutter_vertical = _process_integer process_column_span = _process_integer process_row_span = _process_integer - process_table_size_columns = _process_integer - process_table_size_rows = _process_integer + process_grid_size_columns = _process_integer + process_grid_size_rows = _process_integer - def process_table_gutter(self, name: str, tokens: list[Token]) -> None: + def process_grid_gutter(self, name: str, tokens: list[Token]) -> None: if not tokens: return if len(tokens) == 1: @@ -899,25 +899,25 @@ class StylesBuilder: if token.name != "number": self.error(name, token, integer_help_text(name)) value = max(0, int(token.value)) - self.styles._rules["table_gutter_horizontal"] = value - self.styles._rules["table_gutter_vertical"] = value + self.styles._rules["grid_gutter_horizontal"] = value + self.styles._rules["grid_gutter_vertical"] = value elif len(tokens) == 2: token = tokens[0] if token.name != "number": self.error(name, token, integer_help_text(name)) value = max(0, int(token.value)) - self.styles._rules["table_gutter_horizontal"] = value + self.styles._rules["grid_gutter_horizontal"] = value token = tokens[1] if token.name != "number": self.error(name, token, integer_help_text(name)) value = max(0, int(token.value)) - self.styles._rules["table_gutter_vertical"] = value + self.styles._rules["grid_gutter_vertical"] = value else: self.error(name, tokens[0], "expected two integers here") - def process_table_size(self, name: str, tokens: list[Token]) -> None: + def process_grid_size(self, name: str, tokens: list[Token]) -> None: if not tokens: return if len(tokens) == 1: @@ -925,20 +925,20 @@ class StylesBuilder: if token.name != "number": self.error(name, token, integer_help_text(name)) value = max(0, int(token.value)) - self.styles._rules["table_size_columns"] = value - self.styles._rules["table_size_rows"] = 0 + self.styles._rules["grid_size_columns"] = value + self.styles._rules["grid_size_rows"] = 0 elif len(tokens) == 2: token = tokens[0] if token.name != "number": self.error(name, token, integer_help_text(name)) value = max(0, int(token.value)) - self.styles._rules["table_size_columns"] = value + self.styles._rules["grid_size_columns"] = value token = tokens[1] if token.name != "number": self.error(name, token, integer_help_text(name)) value = max(0, int(token.value)) - self.styles._rules["table_size_rows"] = value + self.styles._rules["grid_size_rows"] = value else: self.error(name, tokens[0], "expected two integers here") diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index 124ecb9bc..38a718a6a 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -32,7 +32,7 @@ VALID_BORDER: Final[set[EdgeType]] = { "wide", } VALID_EDGE: Final = {"top", "right", "bottom", "left"} -VALID_LAYOUT: Final = {"vertical", "horizontal", "center", "table"} +VALID_LAYOUT: Final = {"vertical", "horizontal", "center", "grid"} VALID_BOX_SIZING: Final = {"border-box", "content-box"} VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"} diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index a476460a9..40f744513 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -146,12 +146,12 @@ class RulesMap(TypedDict, total=False): content_align_horizontal: AlignHorizontal content_align_vertical: AlignVertical - table_size_rows: int - table_size_columns: int - table_gutter_horizontal: int - table_gutter_vertical: int - table_rows: tuple[Scalar, ...] - table_columns: tuple[Scalar, ...] + grid_size_rows: int + grid_size_columns: int + grid_gutter_horizontal: int + grid_gutter_vertical: int + grid_rows: tuple[Scalar, ...] + grid_columns: tuple[Scalar, ...] row_span: int column_span: int @@ -267,13 +267,13 @@ class StylesBase(ABC): content_align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top") content_align = AlignProperty() - table_rows = ScalarListProperty() - table_columns = ScalarListProperty() + grid_rows = ScalarListProperty() + grid_columns = ScalarListProperty() - table_size_columns = IntegerProperty(default=1, layout=True) - table_size_rows = IntegerProperty(default=0, layout=True) - table_gutter_horizontal = IntegerProperty(default=0, layout=True) - table_gutter_vertical = IntegerProperty(default=0, layout=True) + grid_size_columns = IntegerProperty(default=1, layout=True) + grid_size_rows = IntegerProperty(default=0, layout=True) + grid_gutter_horizontal = IntegerProperty(default=0, layout=True) + grid_gutter_vertical = IntegerProperty(default=0, layout=True) row_span = IntegerProperty(default=1, layout=True) column_span = IntegerProperty(default=1, layout=True) @@ -805,26 +805,26 @@ class Styles(StylesBase): ) elif has_rule("content_align_vertical"): append_declaration("content-align-vertical", self.content_align_vertical) - elif has_rule("table_columns"): + elif has_rule("grid_columns"): append_declaration( - "table-columns", - " ".join(str(scalar) for scalar in self.table_columns or ()), + "grid-columns", + " ".join(str(scalar) for scalar in self.grid_columns or ()), ) - elif has_rule("table_rows"): + elif has_rule("grid_rows"): append_declaration( - "table-rows", - " ".join(str(scalar) for scalar in self.table_rows or ()), + "grid-rows", + " ".join(str(scalar) for scalar in self.grid_rows or ()), ) - elif has_rule("table_size_columns"): - append_declaration("table-size-columns", str(self.table_size_columns)) - elif has_rule("table_size_rows"): - append_declaration("table-size-rows", str(self.table_size_rows)) - elif has_rule("table_gutter_horizontal"): + elif has_rule("grid_size_columns"): + append_declaration("grid-size-columns", str(self.grid_size_columns)) + elif has_rule("grid_size_rows"): + append_declaration("grid-size-rows", str(self.grid_size_rows)) + elif has_rule("grid_gutter_horizontal"): append_declaration( - "table-gutter-horizontal", str(self.table_gutter_horizontal) + "grid-gutter-horizontal", str(self.grid_gutter_horizontal) ) - elif has_rule("table_gutter_vertical"): - append_declaration("table-gutter-vertical", str(self.table_gutter_vertical)) + elif has_rule("grid_gutter_vertical"): + append_declaration("grid-gutter-vertical", str(self.grid_gutter_vertical)) elif has_rule("row_span"): append_declaration("row-span", str(self.row_span)) elif has_rule("column_span"): diff --git a/src/textual/layouts/factory.py b/src/textual/layouts/factory.py index d0f3de049..2e1320fc4 100644 --- a/src/textual/layouts/factory.py +++ b/src/textual/layouts/factory.py @@ -3,13 +3,13 @@ from __future__ import annotations from .._layout import Layout from .center import CenterLayout from .horizontal import HorizontalLayout -from .table import TableLayout +from .grid import GridLayout from .vertical import VerticalLayout LAYOUT_MAP: dict[str, type[Layout]] = { "center": CenterLayout, "horizontal": HorizontalLayout, - "table": TableLayout, + "grid": GridLayout, "vertical": VerticalLayout, } diff --git a/src/textual/layouts/table.py b/src/textual/layouts/grid.py similarity index 91% rename from src/textual/layouts/table.py rename to src/textual/layouts/grid.py index 0f5a78dea..ade953a31 100644 --- a/src/textual/layouts/table.py +++ b/src/textual/layouts/grid.py @@ -12,21 +12,21 @@ if TYPE_CHECKING: from ..widget import Widget -class TableLayout(Layout): - """Used to layout Widgets in to a table.""" +class GridLayout(Layout): + """Used to layout Widgets in to a grid.""" - name = "table" + name = "grid" def arrange( self, parent: Widget, children: list[Widget], size: Size ) -> ArrangeResult: styles = parent.styles - row_scalars = styles.table_rows or [Scalar.parse("1fr")] - column_scalars = styles.table_columns or [Scalar.parse("1fr")] - gutter_horizontal = styles.table_gutter_horizontal - gutter_vertical = styles.table_gutter_vertical - table_size_columns = max(1, styles.table_size_columns) - table_size_rows = styles.table_size_rows + row_scalars = styles.grid_rows or [Scalar.parse("1fr")] + column_scalars = styles.grid_columns or [Scalar.parse("1fr")] + gutter_horizontal = styles.grid_gutter_horizontal + gutter_vertical = styles.grid_gutter_vertical + table_size_columns = max(1, styles.grid_size_columns) + table_size_rows = styles.grid_size_rows viewport = parent.screen.size def cell_coords(column_count: int) -> Iterable[tuple[int, int]]: From f1a8c74649f35da1ed99c2df5e33527ff99c1076 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 10:23:28 +0100 Subject: [PATCH 11/24] remove copose idea --- src/textual/_compose.py | 44 ----------------------------------------- src/textual/app.py | 6 +----- src/textual/widget.py | 6 +----- 3 files changed, 2 insertions(+), 54 deletions(-) delete mode 100644 src/textual/_compose.py diff --git a/src/textual/_compose.py b/src/textual/_compose.py deleted file mode 100644 index 0a2a01754..000000000 --- a/src/textual/_compose.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - -from types import GeneratorType -from typing import TYPE_CHECKING, Iterable - -if TYPE_CHECKING: - from .app import ComposeResult - from .widget import Widget - - -def _compose(compose_result: ComposeResult) -> Iterable[Widget]: - """Turns a compose result in to an iterable of Widgets. - - If `compose_result` is a generator, this will run the generator and send - back the yielded widgets. This allows you to write code such as this: - - ```python - yes = yield Button("Yes") - yes.variant = "success" - ``` - - Otherwise `compose_result` is assumed to already be an iterable of Widgets - and will be returned unmodified. - - Args: - compose_result (ComposeResult): Either an iterator of widgets, - or a generator. - - Returns: - Iterable[Widget]: An iterable if widgets. - - """ - - if isinstance(compose_result, GeneratorType): - try: - widget = next(compose_result) - send = compose_result.send - while True: - yield widget - widget = send(widget) - except StopIteration: - pass - else: - yield from compose_result diff --git a/src/textual/app.py b/src/textual/app.py index 62acf53b5..5fb4c814b 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -15,7 +15,6 @@ from typing import Any, Generator, Generic, Iterable, Iterator, Type, TypeVar, c from weakref import WeakSet, WeakValueDictionary from ._ansi_sequences import SYNC_END, SYNC_START -from ._compose import _compose from ._path import _make_path_object_relative if sys.version_info >= (3, 8): @@ -386,9 +385,6 @@ class App(Generic[ReturnType], DOMNode): return yield - def _compose(self) -> Iterable[Widget]: - return _compose(self.compose()) - def get_css_variables(self) -> dict[str, str]: """Get a mapping of variables used to pre-populate CSS. @@ -1149,7 +1145,7 @@ class App(Generic[ReturnType], DOMNode): self.set_timer(screenshot_timer, on_screenshot, name="screenshot timer") def _on_mount(self) -> None: - widgets = self._compose() + widgets = self.compose() if widgets: self.mount_all(widgets) diff --git a/src/textual/widget.py b/src/textual/widget.py index 63a332447..a97f8b129 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -18,7 +18,6 @@ from rich.text import Text from . import errors, events, messages from ._animator import BoundAnimator from ._arrange import DockArrangeResult, arrange -from ._compose import _compose from ._context import active_app from ._layout import Layout from ._segment_tools import align_lines @@ -268,9 +267,6 @@ class Widget(DOMNode): return yield - def _compose(self) -> Iterable[Widget]: - return _compose(self.compose()) - def _post_register(self, app: App) -> None: """Called when the instance is registered. @@ -1456,7 +1452,7 @@ class Widget(DOMNode): await self.dispatch_key(event) def _on_mount(self, event: events.Mount) -> None: - widgets = self._compose() + widgets = self.compose() self.mount(*widgets) self.screen.refresh(repaint=False, layout=True) From 3e41e14714aabe619abda1698948b03ff5136ce5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 10:50:12 +0100 Subject: [PATCH 12/24] box diagram --- docs/guide/events.md | 6 ++++++ docs/guide/reactivity.md | 7 +++++++ docs/guide/screens.md | 9 +++++++++ docs/guide/styles.md | 12 +++++++++++- docs/guide/widgets.md | 8 ++++++++ docs/images/styles/box.excalidraw.svg | 16 ++++++++++++++++ mkdocs.yml | 2 +- 7 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 docs/images/styles/box.excalidraw.svg diff --git a/docs/guide/events.md b/docs/guide/events.md index 50d552bbe..7ad07b04c 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -1,5 +1,11 @@ ## Events +- What are events +- Handling events +- Auto calling base classes +- Event bubbling +- Posting / emitting events +
--8<-- "docs/images/test.excalidraw.svg"
diff --git a/docs/guide/reactivity.md b/docs/guide/reactivity.md index 5a26ca9fb..ff6dc0496 100644 --- a/docs/guide/reactivity.md +++ b/docs/guide/reactivity.md @@ -1 +1,8 @@ # Reactivity + +- What is reactivity +- Reactive variables + - Demo + - repaint vs layout +- Validation +- Watch methods diff --git a/docs/guide/screens.md b/docs/guide/screens.md index 5881073ec..4627a5652 100644 --- a/docs/guide/screens.md +++ b/docs/guide/screens.md @@ -1 +1,10 @@ # Screens + +- Explanation of screens +- Screens API + - Install screen + - Uninstall screen + - Push screen + - Pop screen + - Switch Screen +- Screens example diff --git a/docs/guide/styles.md b/docs/guide/styles.md index 5f968b8aa..dddf88929 100644 --- a/docs/guide/styles.md +++ b/docs/guide/styles.md @@ -1,3 +1,13 @@ # Styles -TODO: discussion of box model +- What are styles +- Styles object on widgets / app +- Setting styles via CSS +- Box model +- Color / Background +- Borders / Outline + + +
+--8<-- "docs/images/styles/box.excalidraw.svg" +
diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 097bbeec9..f469dcedf 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -1 +1,9 @@ # Widgets + +- What is a widget +- Defining a basic widget + - Base classes Widget or Static + - Text widgets + - Rich renderable widgets +- Complete widget +- Render line widget API diff --git a/docs/images/styles/box.excalidraw.svg b/docs/images/styles/box.excalidraw.svg new file mode 100644 index 000000000..68e58174f --- /dev/null +++ b/docs/images/styles/box.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1bbU/bylx1MDAxMv7eX4E4X4u7u7Ovla6ugMJcdTAwMDVcbilvhbZXR5WJXHUwMDFkYkji1HaAtOp/v7NcdTAwMGXEdlx1MDAxMpskXHUwMDA0XHUwMDFhjq6llsRre8e788w8z+zm15uVldWk3/VX36+s+nd1t1x1MDAxNXiRe7v61p6/8aM4XGI72MTS73HYi+rplc0k6cbv371ru9G1n3Rbbt13boK457bipOdcdTAwMDWhU1x1MDAwZtvvgsRvx/+2/9fctv+vbtj2ksjJOlnzvSBcdKNBX37Lb/udJMan/1x1MDAxN7+vrPxK/89ZXHUwMDE3+fXE7Vxctvz0hrQpM5BcdTAwMTI1erZcdTAwMTZ2UmOZXHUwMDEyhFx1MDAwMGV8eEFcdTAwMTB/wO5cdTAwMTLfw9ZcdTAwMDaa7Gct9tRq/+zO1Vu19f32pzA6i086YaPuZb02glbrJOm3XHUwMDA2I+HWm70oZ1OcROG1f1x1MDAxZXhJ09o1cn54n1x1MDAxNybWgGFzXHUwMDE09i6bXHUwMDFkP45cdTAwMGI3hV23XHUwMDFlJP30XHUwMDA1yfDsYFx1MDAxNN6vZGfu8Fx1MDAxYqfUoYxcbsmBUcmlgWGzfVx1MDAwMCPa0VJcdTAwMTlcdTAwMDIgiJFU6lx1MDAxMdM2w1x1MDAxNk5cdTAwMDaa9lx1MDAxN0mPzLZcdTAwMGK3fn2JXHUwMDA2drzhNUnkduKuXHUwMDFi4ZRl193evzQ34HAjiVx1MDAwMSWGjU0/uGwm2FxuXHUwMDAyXHUwMDFjiYZInWuN/XQ2KKFAQenszWy/3V0v9Yu/R1x1MDAwN7PpRt37MVuN7ZeczdbcrZxTZTf3up47mHwqJeNCXHUwMDExIyRkg95cbjrX2NjptVrZubB+nflLevb32zn8lDFW5qeUXHUwMDBiqTTO39R+ur+z22qEtfpOo90/vlxmbvaP2HZcXOKncYigm9lLR+56zEnhUVx1MDAxZlXSXHUwMDExQlx0pVxyZ5JzVnRRbDU4XHUwMDA0wFxm01xcXHUwMDExUeqholx1MDAwMV6dV3roX7wu/YZcdTAwMTj3Tlx1MDAxMMrRQvCC/1xyvZNR4ihpXHUwMDA0XHUwMDA2jDHvZFxiXHUwMDFkSVx1MDAwNdeL9877hsydctNcXP+0/7X5Zeez1zem06vRrmp6wfBZXHUwMDA133OjKLxdXHUwMDFktvy+/1Tu+9JIxah6XHUwMDE539ey1PcxXHUwMDFjacLVXGZB+nr/6qpcdTAwMWRHP84j/y4yW1+2vvX4h6VcdTAwMGbSginrXlx1MDAwNjiXXHUwMDA0mOBcdTAwMDVcdTAwMDBcdTAwMDDj6JpcdTAwMThcdTAwMDBcdTAwMTW2XHUwMDE5Y8pDtC/xmieEaCaEwzRcdTAwMTBBJSZcZp6bmSFcdTAwMTIoYVx1MDAwZbZcYmG0MJRrkGN4IOg+XGKIXFzIelx1MDAxMUCs6Y2L/uXeoTigVz9cdTAwMGbXXHUwMDBmhdf1XHUwMDBlp1x1MDAwM8Tbyc99uDzx75Lig1x1MDAwNj1+/LZ+cPZjZ+fbxs636PuthuBuw/2DOCvYmadBOVx1MDAwMI1DjOBMXHUwMDAzJ1ND7CYgwLdNTfWOPtSvL8zR3mf5acFcdTAwMTCbMcNMgTBcdFx1MDAwZdH2XHUwMDAwQTmRxVx1MDAxNIPgcohiXHUwMDE4+3EwXHUwMDEwQubZSFCu4yy5TCA8QjJcdTAwMDWMPUNOqfJGLZFcdTAwMDXO4I3ZpIed5CT4mVx1MDAxMmtSOLvttoNWvzBvqZuipVx1MDAwN250XHUwMDE5dPJjXHUwMDE5+9hn6pa6cPV6K7i0jrza8lx1MDAxYkVcdTAwMGZPXHUwMDAy1Fxyw+YkzL15XHUwMDFke3fxcdGuN/pcdTAwMTZhXHUwMDE0YM9u67RoSSW4XHUwMDA2MWNcdTAwMDK6VI4zj6JLM8W1glx1MDAxYzd4XGZd3oHXYac/jj60vnxsXHUwMDFmt2Rf8k+X86GLzYEuMlx1MDAxZrqAYIbiXHUwMDAyXHUwMDE1XHUwMDA2IIJAXHUwMDE00MWFdkBJ/CMpNuvnk1x1MDAxOEjSXHUwMDE0aEWpRl9GraFgXHUwMDAy2pijOSohinxcdTAwMTM4YPhcdTAwMWJDXHUwMDFmNVxuU1x1MDAxY+Vz6I3U2DnRN2suyNnhRslG0PGCzuXoLX7HK2lpuXGyXHUwMDE5ttuBZTOHYdBJRq9In7tu/b7pu2Mgwifn20ZcdTAwMDHStU8sJu3s00rmQOmX4ee/306+unRi7TE+pdnz3uT/zoxtVJ6lqVx1MDAxM4jmiG85PTmtZijLjG3JQGtcdTAwMDPAZY7VXHKwLVx1MDAxY0DAcIOfkGfIXHUwMDExu1x1MDAxNoltMFxmKSmqco0xRFx1MDAxOTpcdNzCXHUwMDExmOKJXHUwMDA2jpGXoqhcdTAwMWJcdTAwMDW3Ylx1MDAxOKhcYskm7Vx1MDAxZlx1MDAwMu2MuD5cdTAwMTTNdqdcdTAwMTBEaZKu91LvcFxmalx1MDAwZjA4l1x1MDAwNIWGyFKwXHUwMDFkJLdrR9fhXGZcdTAwMTThjFx1MDAxMYZcdTAwMTeg8L2/4nf2XHUwMDFhryqolHqUPdbGnWnGsFLGx005YaBGa6FpXHUwMDA270erknHn48mRd7XlNnd39q9l5+gmbr6g4J0zqCBcdTAwMWRnxFx1MDAwMOWoPTRGj2JQ4cKRxJJxhYRUieej4yqLXHUwMDAz1XRcXFx1MDAxOVCGvDBcdTAwMWSfO2rMRcdcdTAwMGZdL40lS8DHXHUwMDFmTJlcdTAwMGJdnJaiy1xuPFxys4jdyVx1MDAxMn/p4Vx1MDAwNayqniS5g6iS1sOYyEFg4eUkPqmUXG6j8JJcdTAwMDYzXHTR8lx1MDAxZoQuXHUwMDE4Q9cmNqNVKzhU7mSImclcdTAwMTCr411+VFx1MDAwMbJ24Hn50mpcdTAwMTFnj1VER6FXsLNcdTAwMTJ/1VVdU45CYoMpXHUwMDAxOv2Sxsfjy+bOxXbN9JtXZ5e100h8ubstQWE9XG7jeK3pJvVmXHUwMDE5XHUwMDEyRz3++cpO6cqGNoJSYqShXHUwMDE0ikhEWuEgTFx1MDAxOTJcZqRcIpLScixOsbZRicX51zdQ5Vx1MDAxOFwiiHrhcu63XHUwMDBm392Nn+v11ueT//TbXHUwMDA3V+vHR1c8R1xcV2Yu51x1MDAwZZ57sHe3ZcLG3raITte+3rV+XHUwMDA2XHUwMDA36npB6yZcdTAwMGJeMyyXpUyWLlx1MDAxOFx1MDAxYbtSXHUwMDA2s6yZVI/0zOh6OWmqlHJQd1x1MDAwMqOCaJLL+2lNV1pcdTAwMTKJboupjnFaXnR6KrJcZncoKCFcZpph61x1MDAwMVx1MDAxM+CFdoItfaGKsKUlM5ZcdTAwMDKRmUhcdTAwMDCm9Fx1MDAxYzlwXHUwMDE5dGmZ+uyJ/llPbJLrgJyct2X76kadhZPVp9VgXHUwMDAyR8BaZajOSbF79YkhXG4o1Vx1MDAxOE1cciVcdTAwMTg55UOVJ1OfUyni6mRSsFx0QCqcXHUwMDE27FxuaVx1MDAxNFx1MDAxNWrcKOZoalx1MDAxN1x1MDAxNSRcdTAwMDeO4s2AXHUwMDE4M+o1SeK1UmdOW8f8OHvem/zfWSl7niiO7tPRQFx1MDAxNUg2PWWv9rllXHUwMDBlZ2BcdTAwMWOBbEBcdLBcdTAwMGK9vFhpwzDmXHUwMDE4XGb8QiBZQIX8pG1cdTAwMTDVa1RcdTAwMTOWfSfRXHUwMDAyxCFcdTAwMTNyjkraXHUwMDFmolx1MDAwNY+m75dW3Fx1MDAxYmHk5an9n1x1MDAxM9z3lsxcdTAwMDXeXG69LbjiqDD1XGZ6+3tjrb5zur/VPdnvX4ige3F08X1+8C5Mc8tHJTeRXHUwMDBlQ/dRhlwi6Vx1MDAwNyiSXHUwMDExKm3tlFx1MDAwMTWY5FxikzBiV/n+pPI9TFx1MDAwMOPQRZ0xXHJ2qV3eSlxyfVnwXlx1MDAxY9Pzg/bPsH/WIZuHJ5+3T2r7vYVxb8zCuWj07OBcdTAwMWROzFJcdTAwMDA4s2Y+SVGOYkWEVGKGmln1LC9xXHUwMDAyXHUwMDE2klhcZkvkcVx1MDAwMlx1MDAwNCEjXHUwMDE4NsJcdTAwMTFcdTAwMDSMSpfCXHUwMDE0XHUwMDAxNWLYTGXpXG5cdTAwMTAzXHUwMDA3u6ZMc1x1MDAwMlx1MDAwMrTQWT9DSFx1MDAxYmRcbooysKpHXHUwMDE4pXJjdI9wXHLGpFx1MDAwNfSXXHUwMDE0XHUwMDE0iEFcIuVzXG6K6vxQIO+EXHUwMDEyu7dcdMVcdTAwMDRTilx1MDAxM8F57rJcdTAwMDF711x1MDAwZdFUXG7OiKVURLJqRfFql8/XSl3KXHUwMDFlY860IFafy0VjxMBcdTAwMTBhzZm+SPH9lsSb58dcdTAwMDe9g2bbdGRtj3W3xGtcYiqKXHUwMDFix1Y7KVx1MDAwZTtcdTAwMTCSQ/OAXHUwMDE4XHUwMDEwh2qCKkfaaSFP2ttZXHUwMDExU8RUvEAjXGbQXHUwMDAz5qlCPIVcdTAwMTacX3D/cPe85l7v7GyefdZi7+Rqyp2bS8fpXHUwMDA3k7FcdTAwMDSMYGDIfGSgvHhv7HIs5IvBj1x1MDAwMbd6cpdcdTAwMTm4iiFcdTAwMWLghHIkXHUwMDAzjI2SXHUwMDAxJPTKboGmUjND2Khdi1wiXHUwMDAzXHUwMDE4N1x1MDAxY1x1MDAwMKY1/jOY02HCXHUwMDA2UqqpXS9cdTAwMDckacb+TFx1MDAwNnJR5IFcdTAwMGVg9Fx1MDAwMDnXT2iWub5YnVx1MDAxNXJ0YM1cdTAwMTZcdTAwMGZcdTAwMDUyO6aIIVSpXFxh655cZiiHXHUwMDE5jNCKa8ap4fCPJVx1MDAwM+UuZY9xZ5qRXHUwMDBllK9aUFXKXGJwWizMZqhcdTAwMTRUr+Ms7WY66qBcdTAwMGLi4NqfXGZJM7Jccp2Bg1FHa2W3y5qKXHUwMDEy39M30zGDzk5cdTAwMDVcdTAwMTJcdTAwMGZFqMkvTGY/ybO/12PEKJpSj1xcSeMhqlxisOVxwl94N52xhH5cdTAwMDFRZfbddDOsXHUwMDFkSIUqkigtXHLBkZY0d9Eg3nBH253GQqCwXHUwMDA0jsRwLN68pqhS4VH2WFx1MDAxYnOmRUVcdTAwMTUjS38/hpzablrV0+++P2zou5qutS42k6vT069f6ipufHxcckFFSUxcXEZiQGd0ZN2AocpcdTAwMDVqdzpqLXXFdp+nXHUwMDA3XHUwMDE11NqEo6wzoFx1MDAxMaFyQuHCmqrRSmLtlZTnXG52w6BCJKBcdTAwMTid57cvT1xuKlx1MDAxOH2zkfv/7vtcdTAwMDK2yybWXHUwMDFl41NaXHUwMDA27Tf3Pay63e5JguM/jLQ4xYF3r7Wy11xcvVx0/NuNXHR0uZFcdTAwMWVW3KThwuLSty/76/eb3/9cdTAwMDOnMe1cdTAwMTYifQ== + + + + MarginPaddingContent areaBorderBackgroundColor \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 2bbbb0c94..58f29d109 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,8 +8,8 @@ nav: - Guide: - "guide/devtools.md" - "guide/app.md" - - "guide/CSS.md" - "guide/styles.md" + - "guide/CSS.md" - "guide/layout.md" - "guide/events.md" - "guide/actions.md" From 9aa7228fda82432126d75a4767ef047753ba724c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 13:11:06 +0100 Subject: [PATCH 13/24] added doc indexes --- docs/events/index.md | 3 +++ docs/guide/index.md | 9 +++++++++ docs/how-to/index.md | 3 +++ docs/reference/index.md | 3 +++ docs/styles/index.md | 3 +++ docs/widgets/index.md | 3 +++ mkdocs.yml | 14 ++++++++++---- poetry.lock | 2 +- 8 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 docs/events/index.md create mode 100644 docs/guide/index.md create mode 100644 docs/how-to/index.md create mode 100644 docs/reference/index.md create mode 100644 docs/styles/index.md create mode 100644 docs/widgets/index.md diff --git a/docs/events/index.md b/docs/events/index.md new file mode 100644 index 000000000..5b3430c34 --- /dev/null +++ b/docs/events/index.md @@ -0,0 +1,3 @@ +# Events + +A reference to Textual [events](../guide/events.md). diff --git a/docs/guide/index.md b/docs/guide/index.md new file mode 100644 index 000000000..10c6e3a0f --- /dev/null +++ b/docs/guide/index.md @@ -0,0 +1,9 @@ +# Textual Guide + +Welcome to the Textual Guide! An in-depth reference on how to build app with Textual. + +## Example code + +Most of the code in this guide is fully working—you could cut and paste it if you wanted to. + +Although it is probably easier to check out the [Textual repository](https://github.com/Textualize/textual) and navigate to the `docs/examples/guide` directory and run the examples from there. diff --git a/docs/how-to/index.md b/docs/how-to/index.md new file mode 100644 index 000000000..48a0aac26 --- /dev/null +++ b/docs/how-to/index.md @@ -0,0 +1,3 @@ +# How to ... + +For those who want more focused information on Textual features. diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 000000000..8e129acd1 --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,3 @@ +# Reference + +A reference to the Textual public APIs. diff --git a/docs/styles/index.md b/docs/styles/index.md new file mode 100644 index 000000000..67b6f6c85 --- /dev/null +++ b/docs/styles/index.md @@ -0,0 +1,3 @@ +# Styles + +A reference to Widget [styles](../guide/styles.md). diff --git a/docs/widgets/index.md b/docs/widgets/index.md new file mode 100644 index 000000000..90c80104e --- /dev/null +++ b/docs/widgets/index.md @@ -0,0 +1,3 @@ +# Widgets + +A reference to the builtin [widgets](../guide/widgets.md). diff --git a/mkdocs.yml b/mkdocs.yml index 58f29d109..fe215570e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,7 @@ nav: - "getting_started.md" - "tutorial.md" - Guide: + - "guide/index.md" - "guide/devtools.md" - "guide/app.md" - "guide/styles.md" @@ -17,11 +18,12 @@ nav: - "guide/widgets.md" - "guide/screens.md" - How to: + - "how-to/index.md" - "how-to/animation.md" - "how-to/mouse-and-keyboard.md" - - "how-to/scroll.md" - - "actions.md" + - "how-to/scroll.md" - Events: + - "events/index.md" - "events/blur.md" - "events/descendant_blur.md" - "events/descendant_focus.md" @@ -47,6 +49,7 @@ nav: - "events/screen_suspend.md" - "events/show.md" - Styles: + - "styles/index.md" - "styles/background.md" - "styles/border.md" - "styles/box_sizing.md" @@ -75,6 +78,7 @@ nav: - "styles/visibility.md" - "styles/width.md" - Widgets: + - "widgets/index.md" - "widgets/button.md" - "widgets/data_table.md" - "widgets/footer.md" @@ -82,6 +86,7 @@ nav: - "widgets/static.md" - "widgets/tree_control.md" - Reference: + - "reference/index.md" - "reference/app.md" - "reference/button.md" - "reference/color.md" @@ -128,8 +133,9 @@ markdown_extensions: theme: name: material custom_dir: custom_theme - # features: - # - navigation.tabs + features: + - navigation.tabs + - navigation.indexes palette: - media: "(prefers-color-scheme: light)" scheme: default diff --git a/poetry.lock b/poetry.lock index 1871dd8ed..c6c1d2d27 100644 --- a/poetry.lock +++ b/poetry.lock @@ -345,7 +345,7 @@ mkdocs = ">=1.1" [[package]] name = "mkdocs-material" -version = "8.4.1" +version = "8.4.2" description = "Documentation that simply works" category = "dev" optional = false From 4a7f601205501f1091eeb61375d6d86edb7028a9 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 13:26:55 +0100 Subject: [PATCH 14/24] indexes and tweaks --- docs/actions.md | 1 - docs/guide/app.md | 6 +++--- docs/index.md | 2 +- docs/tutorial.md | 5 +++-- docs/user_guide/messages.md | 42 ------------------------------------- 5 files changed, 7 insertions(+), 49 deletions(-) delete mode 100644 docs/actions.md delete mode 100644 docs/user_guide/messages.md diff --git a/docs/actions.md b/docs/actions.md deleted file mode 100644 index 1060a658b..000000000 --- a/docs/actions.md +++ /dev/null @@ -1 +0,0 @@ -# Actions diff --git a/docs/guide/app.md b/docs/guide/app.md index 70ed8f12f..753648a88 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -41,7 +41,7 @@ One such event is the *mount* event which is sent to an application after it ent !!! info - You may have noticed we use the term "send" and "sent" in relation to event handler methods in preference to "calling". This is because Textual uses a message passing system where events are passed (or *sent*) between components. We will cover the details in [events][events.md]. + You may have noticed we use the term "send" and "sent" in relation to event handler methods in preference to "calling". This is because Textual uses a message passing system where events are passed (or *sent*) between components. We will cover the details in [events][./events.md]. Another such event is the *key* event which is sent when the user presses a key. The following example contains handlers for both those events: @@ -99,7 +99,7 @@ Notice the `on_button_pressed` method which handles the [Button.Pressed][textual ### Mounting -While composing is the preferred way of adding widgets when your app starts it is sometimes necessary to add new widget(s) in response to events. You can do this by calling [mount()](textual.widget.Widget.mount) which will add a new widget to the UI. +While composing is the preferred way of adding widgets when your app starts it is sometimes necessary to add new widget(s) in response to events. You can do this by calling [mount()][textual.widget.Widget.mount] which will add a new widget to the UI. Here's an app which adds the welcome widget in response to any key press: @@ -114,7 +114,7 @@ When you first run this you will get a blank screen. Press any key to add the we ### Exiting -An app will run until you call [App.exit()](textual.app.App.exit) which will exit application mode and the [run](textual.app.App.run) method will return. If this is the last line in your code you will return to the command prompt. +An app will run until you call [App.exit()][textual.app.App.exit] which will exit application mode and the [run][textual.app.App.run] method will return. If this is the last line in your code you will return to the command prompt. The exit method will also accept an optional positional value to be returned by `run()`. The following example uses this to return the `id` (identifier) of a clicked button. diff --git a/docs/index.md b/docs/index.md index dd59b082b..b0b2bcb1d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ Welcome to the [Textual](https://github.com/Textualize/textual) framework docume
-Textual is a framework for building applications that run within your terminal. Such Text User Interfaces (TUIs) have a number of advantages over traditional web and desktop apps. +Textual is a framework for building applications that run within your terminal. Text User Interfaces (TUIs) have a number of advantages over web and desktop apps.
diff --git a/docs/tutorial.md b/docs/tutorial.md index abb70ac6f..fb3c33089 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -68,8 +68,9 @@ def repeat(text: str, count: int) -> str: return text * count ``` -- Parameter types follow a colon. So `text: str` indicates that `text` requires a string and `count: int` means that `count` requires an integer. -- Return types follow `->`. So `-> str:` indicates this method returns a string. +Parameter types follow a colon. So `text: str` indicates that `text` requires a string and `count: int` means that `count` requires an integer. + +Return types follow `->`. So `-> str:` indicates this method returns a string. ## The App class diff --git a/docs/user_guide/messages.md b/docs/user_guide/messages.md deleted file mode 100644 index 1f42bb19d..000000000 --- a/docs/user_guide/messages.md +++ /dev/null @@ -1,42 +0,0 @@ -# Messages & Events - -Each component of a Textual application has it its heart a queue of messages and a task which monitors this queue and calls Python code in response. The queue and task are collectively known as a _message pump_. - -You will most often deal with _events_ which are a particular type of message that are created in response to user actions, such as key presses and mouse clicks, but also internal events such as timers. These events typically originate from a Driver class which sends them to an App class which is where you write code to respond to those events. - -Lets write an _app_ which responds to a key event. This is probably the simplest Textual application that I can conceive of: - -```python -from textual.app import App - - -class Beeper(App): - async def on_key(self, event): - self.console.bell() - - -Beeper.run() -``` - -If you run the above code, Textual will switch the terminal in to _application mode_. The terminal will go blank and the app will start processing events. If you hit any key you should hear a beep. Hit ctrl+C (control key and C key at the same time) to exit application mode and return to the terminal. - -Although simple, this app follows the same pattern as more sophisticated applications. It starts by deriving a class from `App`; in this case `Beeper`. Calling the classmethod `run()` starts the application. - -In our Beeper class there is a single event handler `on_key` which is called in response to a `Key` event. The method name is assumed by concatenating `on_` with the event name, hence `on_key` for a Key event, `on_timer` for a Timer event, etc. In Beeper, the on_key event calls `self.console.bell()` which is what plays the beep noise (if supported by your terminal). - -The `on_key` method is preceded by the keyword `async` making it an asynchronous method. Textual is an asynchronous framework so event handlers and most methods are async. - -Our Beeper app is missing typing information. Although completely optional, I recommend adding typing information which will help catch bugs (using tools such as [Mypy](https://mypy.readthedocs.io/en/stable/)). Here is the Beeper class with added typing: - -```python -from textual.app import App -from textual import events - - -class Beeper(App): - async def on_key(self, event: events.Key) -> None: - self.console.bell() - - -Beeper.run() -``` From b2db7a63abf782a4feecd44bce93d5ebd9ece10b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 15:32:02 +0100 Subject: [PATCH 15/24] deploy command --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 33455a881..a4aab88a8 100644 --- a/Makefile +++ b/Makefile @@ -12,3 +12,6 @@ docs-serve: mkdocs serve docs-build: mkdocs build +docs-deploy: + mkdocs gh-deploy + From d9d5791925fe8184cf71c303a9b12bb970524d6f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 15:39:41 +0100 Subject: [PATCH 16/24] todos --- docs/guide/CSS.md | 4 ++++ docs/guide/actions.md | 2 ++ docs/guide/events.md | 2 ++ docs/guide/layout.md | 2 ++ docs/guide/reactivity.md | 2 ++ docs/guide/screens.md | 2 ++ docs/guide/styles.md | 2 ++ docs/guide/widgets.md | 2 ++ docs/index.md | 12 +++--------- 9 files changed, 21 insertions(+), 9 deletions(-) diff --git a/docs/guide/CSS.md b/docs/guide/CSS.md index 684f7d372..f656f0121 100644 --- a/docs/guide/CSS.md +++ b/docs/guide/CSS.md @@ -391,3 +391,7 @@ Button:hover { background: blue !important; } ``` + +## CSS Variables + +TODO: Variables diff --git a/docs/guide/actions.md b/docs/guide/actions.md index 1060a658b..03e8cb174 100644 --- a/docs/guide/actions.md +++ b/docs/guide/actions.md @@ -1 +1,3 @@ # Actions + +TODO: Actions docs diff --git a/docs/guide/events.md b/docs/guide/events.md index 7ad07b04c..1eea90333 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -1,5 +1,7 @@ ## Events +TODO: events docs + - What are events - Handling events - Auto calling base classes diff --git a/docs/guide/layout.md b/docs/guide/layout.md index c04fea024..051f303ee 100644 --- a/docs/guide/layout.md +++ b/docs/guide/layout.md @@ -2,6 +2,8 @@ In textual the *layout* defines how widgets will be arranged (or *layed out*) on the screen. Textual supports a number of layouts which can be set either via a widgets `styles` object or via CSS. +TODO: layout docs + ## Vertical A vertical layout will place new widgets below previous widgets, starting from the top of the screen. diff --git a/docs/guide/reactivity.md b/docs/guide/reactivity.md index ff6dc0496..b9918c259 100644 --- a/docs/guide/reactivity.md +++ b/docs/guide/reactivity.md @@ -1,5 +1,7 @@ # Reactivity +TODO: Reactivity docs + - What is reactivity - Reactive variables - Demo diff --git a/docs/guide/screens.md b/docs/guide/screens.md index 4627a5652..688684a43 100644 --- a/docs/guide/screens.md +++ b/docs/guide/screens.md @@ -1,5 +1,7 @@ # Screens +TODO: Screens docs + - Explanation of screens - Screens API - Install screen diff --git a/docs/guide/styles.md b/docs/guide/styles.md index dddf88929..b54ad51a8 100644 --- a/docs/guide/styles.md +++ b/docs/guide/styles.md @@ -1,5 +1,7 @@ # Styles +TODO: Styles docs + - What are styles - Styles object on widgets / app - Setting styles via CSS diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index f469dcedf..b566f1750 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -1,5 +1,7 @@ # Widgets +TODO: Widgets docs + - What is a widget - Defining a basic widget - Base classes Widget or Static diff --git a/docs/index.md b/docs/index.md index b0b2bcb1d..02cf08e75 100644 --- a/docs/index.md +++ b/docs/index.md @@ -59,16 +59,10 @@ Textual is a framework for building applications that run within your terminal.
+```{.textual path="docs/examples/demo.py" columns=100 lines=48} - - -=== "Example 1" - - ```{.textual path="docs/examples/demo.py" columns=100 lines=48} - - ``` - -=== "Example 2" +``` +TODO: Add more example screenshots From e4c16b6d6d230946cc2a56e5d550bae7284fe5dd Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 16:06:01 +0100 Subject: [PATCH 17/24] added grid layout --- docs/guide/app.md | 6 ++++-- docs/styles/layout.md | 9 ++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/guide/app.md b/docs/guide/app.md index 753648a88..8e924dc2b 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -129,7 +129,7 @@ Running this app will give you the following: Clicking either of those buttons will exit the app, and the `run()` method will return either `"yes"` or `"no"` depending on button clicked. -#### Typing +#### Return type You may have noticed that we subclassed `App[str]` rather than the usual `App`. @@ -137,7 +137,9 @@ You may have noticed that we subclassed `App[str]` rather than the usual `App`. --8<-- "docs/examples/app/question01.py" ``` -The addition of `[str]` tells Mypy that `run()` is expected to return a string. It may also return `None` if `sys.exit()` is called without a return value, so the return type of `run` will be `str | None`. +The addition of `[str]` tells Mypy that `run()` is expected to return a string. It may also return `None` if [App.exit()][textual.app.App.exit] is called without a return value, so the return type of `run` will be `str | None`. + +You can change the type to match the values you intend to pass to App.exit()][textual.app.App.exit]. !!! note diff --git a/docs/styles/layout.md b/docs/styles/layout.md index 65fdf7998..1ce87bf23 100644 --- a/docs/styles/layout.md +++ b/docs/styles/layout.md @@ -2,19 +2,22 @@ The `layout` property defines how a widget arranges its children. +See [layout](../guide/layout.md) guide for more information. + ## Syntax ``` -layout: [vertical|horizontal|center]; +layout: [center|grid|horizontal|vertical]; ``` ### Values | Value | Description | |----------------------|-------------------------------------------------------------------------------| -| `vertical` (default) | Child widgets will be arranged along the vertical axis, from top to bottom. | -| `horizontal` | Child widgets will be arranged along the horizontal axis, from left to right. | | `center` | A single child widget will be placed in the center. | +| `grid` | Child widgets will be arranged in a grid. | +| `horizontal` | Child widgets will be arranged along the horizontal axis, from left to right. | +| `vertical` (default) | Child widgets will be arranged along the vertical axis, from top to bottom. | ## Example From 11fea02833c4f163875307ef4c0a1cf57ec1df7a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 16:09:15 +0100 Subject: [PATCH 18/24] animator --- docs/guide/animator.md | 3 +++ mkdocs.yml | 1 + 2 files changed, 4 insertions(+) create mode 100644 docs/guide/animator.md diff --git a/docs/guide/animator.md b/docs/guide/animator.md new file mode 100644 index 000000000..5fe440d74 --- /dev/null +++ b/docs/guide/animator.md @@ -0,0 +1,3 @@ +# Animator + +TODO: Animator docs diff --git a/mkdocs.yml b/mkdocs.yml index fe215570e..373a230f8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,6 +16,7 @@ nav: - "guide/actions.md" - "guide/reactivity.md" - "guide/widgets.md" + - "guide/animator.md" - "guide/screens.md" - How to: - "how-to/index.md" From 43c45158ddb02bfab0c73b3449b7d064771fdeea Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 16:32:40 +0100 Subject: [PATCH 19/24] edit --- docs/tutorial.md | 2 +- mkdocs.yml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index fb3c33089..e668de546 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -25,7 +25,7 @@ Here's what the finished app will look like: ### Get the code -If you want to try the finished Stopwatch app and follow along with the code, first make sure you have [Textual installed](getting_started.md) and then check out the [Textual](https://github.com/Textualize/textual) GitHub repository: +If you want to try the finished Stopwatch app and follow along with the code, first make sure you have [Textual installed](getting_started.md) then check out the [Textual](https://github.com/Textualize/textual) repository: === "HTTPS" diff --git a/mkdocs.yml b/mkdocs.yml index 373a230f8..5ad5a5a08 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,8 +27,7 @@ nav: - "events/index.md" - "events/blur.md" - "events/descendant_blur.md" - - "events/descendant_focus.md" - - "events/enter.md" + - "events/descendant_focus.md" - "events/enter.md" - "events/focus.md" - "events/hide.md" From 2128ea62a502661cb2519cd196efa00b4c58bf97 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 16:43:08 +0100 Subject: [PATCH 20/24] added reactive to reference docs --- docs/reference/reactive.md | 1 + docs/tutorial.md | 2 +- src/textual/reactive.py | 41 +++++++++++++++----------------------- 3 files changed, 18 insertions(+), 26 deletions(-) create mode 100644 docs/reference/reactive.md diff --git a/docs/reference/reactive.md b/docs/reference/reactive.md new file mode 100644 index 000000000..b574e77da --- /dev/null +++ b/docs/reference/reactive.md @@ -0,0 +1 @@ +::: textual.reactive.Reactive diff --git a/docs/tutorial.md b/docs/tutorial.md index e668de546..73e2ad7e1 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -338,7 +338,7 @@ If you run "stopwatch04.py" now you will be able to toggle between the two state A recurring theme in Textual is that you rarely need to explicitly update a widget. It is possible: you can call [`refresh()`][textual.widget.Widget.refresh] to display new data. However, Textual prefers to do this automatically via _reactive_ attributes. -You can declare a reactive attribute with `textual.reactive.Reactive`. Let's use this feature to create a timer that displays elapsed time and keeps it updated. +You can declare a reactive attribute with [Reactive][textual.reactive.Reactive]. Let's use this feature to create a timer that displays elapsed time and keeps it updated. ```python title="stopwatch04.py" hl_lines="1 5 12-27" --8<-- "docs/examples/tutorial/stopwatch05.py" diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 99dea0ea0..78ce82f00 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -1,19 +1,10 @@ from __future__ import annotations -from inspect import isawaitable from functools import partial -from typing import ( - Any, - Callable, - Generic, - Type, - Union, - TypeVar, - TYPE_CHECKING, -) +from inspect import isawaitable +from typing import TYPE_CHECKING, Any, Callable, Generic, Type, TypeVar, Union from . import events - from ._callback import count_parameters, invoke from ._types import MessageTarget @@ -31,7 +22,15 @@ T = TypeVar("T") class Reactive(Generic[ReactiveType]): - """Reactive descriptor.""" + """Reactive descriptor. + + Args: + default (ReactiveType | Callable[[], ReactiveType]): A default value or callable that returns a default. + layout (bool, optional): Perform a layout on change. Defaults to False. + repaint (bool, optional): Perform a repaint on change. Defaults to True. + init (bool, optional): Call watchers on initialize (post mount). Defaults to False. + + """ def __init__( self, @@ -41,14 +40,6 @@ class Reactive(Generic[ReactiveType]): repaint: bool = True, init: bool = False, ) -> None: - """Create a Reactive Widget attribute, - - Args: - default (ReactiveType | Callable[[], ReactiveType]): A default value or callable that returns a default. - layout (bool, optional): Perform a layout on change. Defaults to False. - repaint (bool, optional): Perform a repaint on change. Defaults to True. - init (bool, optional): Call watchers on initialize (post mount). Defaults to False. - """ self._default = default self._layout = layout self._repaint = repaint @@ -138,12 +129,12 @@ class Reactive(Generic[ReactiveType]): if current_value != value or first_set: setattr(obj, f"__first_set_{self.internal_name}", False) setattr(obj, self.internal_name, value) - self.check_watchers(obj, name, current_value) + self._check_watchers(obj, name, current_value) if self._layout or self._repaint: obj.refresh(repaint=self._repaint, layout=self._layout) @classmethod - def check_watchers(cls, obj: Reactable, name: str, old_value: Any) -> None: + def _check_watchers(cls, obj: Reactable, name: str, old_value: Any) -> None: internal_name = f"_reactive_{name}" value = getattr(obj, internal_name) @@ -158,7 +149,7 @@ class Reactive(Generic[ReactiveType]): watch_result = watch_function(value) if isawaitable(watch_result): await watch_result - await Reactive.compute(obj) + await Reactive._compute(obj) watch_function = getattr(obj, f"watch_{name}", None) if callable(watch_function): @@ -182,7 +173,7 @@ class Reactive(Generic[ReactiveType]): ) @classmethod - async def compute(cls, obj: Reactable) -> None: + async def _compute(cls, obj: Reactable) -> None: _rich_traceback_guard = True computes = getattr(obj, "__computes", []) for compute in computes: @@ -203,4 +194,4 @@ def watch( setattr(obj, watcher_name, set()) watchers = getattr(obj, watcher_name) watchers.add(callback) - Reactive.check_watchers(obj, attribute_name, current_value) + Reactive._check_watchers(obj, attribute_name, current_value) From e4df14ca5de1ff7a09b5767b283e58ffbff380f8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 21:35:05 +0100 Subject: [PATCH 21/24] micro-optimization --- src/textual/_compositor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 6ceda8e6b..ef5d8b331 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -222,6 +222,7 @@ class Compositor: for y in range(region_y, region_y + height): setdefault(y, []).append(span) + slice_remaining = slice(1, None) for y, ranges in sorted(inline_ranges.items()): if len(ranges) == 1: # Special case of 1 span @@ -229,7 +230,7 @@ class Compositor: else: ranges.sort() x1, x2 = ranges[0] - for next_x1, next_x2 in ranges[1:]: + for next_x1, next_x2 in ranges[slice_remaining]: if next_x1 <= x2: if next_x2 > x2: x2 = next_x2 From ac56d6102f76396f2578a10d06ce19976a689e89 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 21:44:27 +0100 Subject: [PATCH 22/24] fix return --- src/textual/widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index a97f8b129..138059bc3 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -233,7 +233,7 @@ class Widget(DOMNode): # reset the scroll position if the scrollbar is hidden. self.scroll_to(0, 0, animate=False) - def mount(self, *anon_widgets: Widget, **widgets: Widget) -> int: + def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None: """Mount child widgets (making this widget a container). Widgets may be passed as positional arguments or keyword arguments. If keyword arguments, From 22953b90bd95bdb56f75ec74bcbdb8969139dd00 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 6 Sep 2022 21:44:54 +0100 Subject: [PATCH 23/24] type fix --- src/textual/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/app.py b/src/textual/app.py index 5fb4c814b..d7ec74e0e 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -97,7 +97,7 @@ DEFAULT_COLORS = { } -ComposeResult = "Iterable[Widget] | Generator[Widget, Widget, None]" +ComposeResult = Iterable[Widget] class AppError(Exception): From 6247f94fc5636e4792eb9062613c6a8540563330 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 7 Sep 2022 10:32:43 +0100 Subject: [PATCH 24/24] Added text input to widgetS --- docs/widgets/text_input.md | 1 + mkdocs.yml | 1 + 2 files changed, 2 insertions(+) create mode 100644 docs/widgets/text_input.md diff --git a/docs/widgets/text_input.md b/docs/widgets/text_input.md new file mode 100644 index 000000000..c55ce3dde --- /dev/null +++ b/docs/widgets/text_input.md @@ -0,0 +1 @@ +# TextInput diff --git a/mkdocs.yml b/mkdocs.yml index 5ad5a5a08..fbee83021 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -84,6 +84,7 @@ nav: - "widgets/footer.md" - "widgets/header.md" - "widgets/static.md" + - "widgets/text_input.md" - "widgets/tree_control.md" - Reference: - "reference/index.md"