diff --git a/.gitignore b/.gitignore index b175eb0..04c81ff 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ build/ dist/ __pycache__ +site/ +.screenshot_cache/ diff --git a/Makefile b/Makefile index bbf3023..9caed15 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,14 @@ -lib := textual_canvas -src := src/ -run := rye run -test := rye test -python := $(run) python -lint := rye lint -- --select I -fmt := rye fmt -mypy := $(run) mypy -mkdocs := $(run) mkdocs -spell := $(run) codespell +lib := textual_canvas +src := src/ +examples := docs/examples +run := rye run +test := rye test +python := $(run) python +lint := rye lint -- --select I +fmt := rye fmt +mypy := $(run) mypy +mkdocs := $(run) mkdocs +spell := $(run) codespell ############################################################################## # Local "interactive testing" of the code. @@ -42,23 +43,23 @@ resetup: realclean # Recreate the virtual environment from scratch # Checking/testing/linting/etc. .PHONY: lint lint: # Check the code for linting issues - $(lint) $(src) + $(lint) $(src) $(examples) .PHONY: codestyle codestyle: # Is the code formatted correctly? - $(fmt) --check $(src) + $(fmt) --check $(src) $(examples) .PHONY: typecheck typecheck: # Perform static type checks with mypy - $(mypy) --scripts-are-modules $(src) + $(mypy) --scripts-are-modules $(src) $(examples) .PHONY: stricttypecheck stricttypecheck: # Perform a strict static type checks with mypy - $(mypy) --scripts-are-modules --strict $(src) + $(mypy) --scripts-are-modules --strict $(src) $(examples) .PHONY: spellcheck spellcheck: # Spell check the code - $(spell) *.md $(src) $(docs) $(tests) + $(spell) *.md $(src) $(docs) .PHONY: checkall checkall: spellcheck codestyle lint stricttypecheck # Check all the things @@ -103,19 +104,26 @@ repl: # Start a Python REPL in the venv. .PHONY: delint delint: # Fix linting issues. - $(lint) --fix $(src) + $(lint) --fix $(src) $(examples) .PHONY: pep8ify pep8ify: # Reformat the code to be as PEP8 as possible. - $(fmt) $(src) + $(fmt) $(src) $(examples) .PHONY: tidy tidy: delint pep8ify # Tidy up the code, fixing lint and format issues. -.PHONY: clean -clean: # Clean the build directories +.PHONY: clean-packaging +clean-packaging: # Clean the package building files rm -rf dist +.PHONY: clean-docs +clean-docs: # Clean up the documentation building files + rm -rf site .screenshot_cache + +.PHONY: clean +clean: clean-packaging clean-docs # Clean the build directories + .PHONY: realclean realclean: clean # Clean the venv and build directories rm -rf .venv diff --git a/README.md b/README.md index 78ee326..801fd88 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,11 @@ ## Introduction -This library aims to provide a very simple terminal-based drawing canvas -widget for use with [Textual](https://textual.textualize.io/). Initially -developed for a (also-being-worked-on-right-now) less general project, I'm -making it available on the off-chance anyone else might want to play with it -now. - -You might not want to rely on this just yet though; I'm still messing and -experimenting. +`textual-canvas` provides a simple terminal-based drawing canvas widget for +use with [Textual](https://textual.textualize.io/). Initially developed as a +widget for building +[`textual-mandelbrot`](https://github.com/davep/textual-mandelbrot), it made +sense to spin it out into its own general-purpose library. ## Installing @@ -30,98 +27,7 @@ This is a scrollable and focusable widget that can be used to colour "pixels" themselves are half a character cell in height, hopefully coming out roughly square in most environments. -The `Canvas` can be imported like this: - -```python -from textual_canvas import Canvas -``` - -And is created by providing a width and height (in its own idea of "pixels") -plus an optional initial background colour (which should be a Textual -[`Color`](https://textual.textualize.io/api/color/#textual.color.Color)). - -In a Textual `compose` method you might use it something like this: - -```python -yield Canvas(120, 120, Color(30, 40, 50)) -``` - -Currently there are the following methods available for drawing: - -- `clear(self, color: Color | None = None) -> Self` -- `set_pixel(self, x: int, y: int, color: Color) -> Self` -- `set_pixels(self, locations: Iterable[ tuple[ int, int ] ], color: Color) -> Self` -- `draw_line(self, x0: int, y0: int, x1: int, y1: int, color: Color) -> Self` -- `draw_rectangle(self, x: int, y: int, width: int, height: int, color: Color) -> Self` -- `draw_circle(self, center_x: int, center_y: int, radius: int, color: Color) -> Self` - -I'll document all of this better, when I spend more time on it than the 1/2 -hour somewhere between dinner and bedtime. - -A quick and dirty example of this being used would be: - -```python -from textual.app import App, ComposeResult -from textual.color import Color -from textual_canvas import Canvas - - -############################################################################## -class CanvasTestApp(App[None]): - """The Canvas testing application.""" - - CSS = """ - Canvas { - border: round green; - width: 1fr; - height: 1fr; - } - """ - - def compose(self) -> ComposeResult: - yield Canvas(120, 120, Color(30, 40, 50)) - - def on_mount(self) -> None: - """Set up the display once the DOM is available.""" - canvas = self.query_one(Canvas) - - canvas.draw_line(60, 40, 90, 80, Color(128, 128, 128)) - canvas.draw_line(60, 40, 30, 80, Color(128, 128, 128)) - canvas.draw_line(30, 80, 90, 80, Color(128, 128, 128)) - - canvas.draw_line(0, 70, 48, 55, Color(255, 255, 255)) - - for n in range(52, 59): - canvas.draw_line(48, 55, 58, n, Color(128, 128, 128)) - - canvas.draw_line(70, 52, 119, 57, Color(255, 0, 0)) - canvas.draw_line(71, 53, 119, 58, Color(255, 165, 0)) - canvas.draw_line(72, 54, 119, 59, Color(255, 255, 0)) - canvas.draw_line(72, 55, 119, 60, Color(0, 255, 0)) - canvas.draw_line(73, 56, 119, 61, Color(0, 0, 255)) - canvas.draw_line(74, 57, 119, 62, Color(75, 0, 130)) - canvas.draw_line(75, 58, 119, 63, Color(143, 0, 255)) - - -if __name__ == "__main__": - CanvasTestApp().run() -``` - -To see this code in action you, in an environment where the library is -installed, run: - -```sh -$ python -m textual_canvas -``` - -You should see something like this: - -![Demo code](https://raw.githubusercontent.com/davep/textual-canvas/main/img/textual-canvas.png) - -## TODO - -Lots. Lots and lots. As mentioned above, there's little tinker project that -I'm building on top of this, so I'll be using that to see how this gets -fleshed out. +See [the documentation](https://textual-canvas.davep.dev/) for an +introduction, a usage guide and detailed API documentation. [//]: # (README.md ends here) diff --git a/docs/examples/default_background.py b/docs/examples/default_background.py new file mode 100644 index 0000000..454dbc4 --- /dev/null +++ b/docs/examples/default_background.py @@ -0,0 +1,19 @@ +from textual.app import App, ComposeResult + +from textual_canvas import Canvas + + +class DefaultBackgroundApp(App[None]): + CSS = """ + Canvas { + border: solid black; + background: cornflowerblue; + } + """ + + def compose(self) -> ComposeResult: + yield Canvas(30, 30) # (1)! + + +if __name__ == "__main__": + DefaultBackgroundApp().run() diff --git a/docs/examples/draw_circle.py b/docs/examples/draw_circle.py new file mode 100644 index 0000000..1340b6c --- /dev/null +++ b/docs/examples/draw_circle.py @@ -0,0 +1,23 @@ +from textual.app import App, ComposeResult +from textual.color import Color + +from textual_canvas import Canvas + + +class DrawCircleApp(App[None]): + CSS = """ + Canvas { + background: $panel; + color: blue; + } + """ + + def compose(self) -> ComposeResult: + yield Canvas(30, 30, Color.parse("cornflowerblue")) + + def on_mount(self) -> None: + self.query_one(Canvas).draw_circle(14, 14, 10) + + +if __name__ == "__main__": + DrawCircleApp().run() diff --git a/docs/examples/draw_line.py b/docs/examples/draw_line.py new file mode 100644 index 0000000..e8d476b --- /dev/null +++ b/docs/examples/draw_line.py @@ -0,0 +1,23 @@ +from textual.app import App, ComposeResult +from textual.color import Color + +from textual_canvas import Canvas + + +class DrawLineApp(App[None]): + CSS = """ + Canvas { + background: $panel; + color: blue; + } + """ + + def compose(self) -> ComposeResult: + yield Canvas(30, 30, Color.parse("cornflowerblue")) + + def on_mount(self) -> None: + self.query_one(Canvas).draw_line(2, 2, 27, 27) + + +if __name__ == "__main__": + DrawLineApp().run() diff --git a/docs/examples/draw_rectangle.py b/docs/examples/draw_rectangle.py new file mode 100644 index 0000000..ee099ae --- /dev/null +++ b/docs/examples/draw_rectangle.py @@ -0,0 +1,23 @@ +from textual.app import App, ComposeResult +from textual.color import Color + +from textual_canvas import Canvas + + +class DrawRectangleApp(App[None]): + CSS = """ + Canvas { + background: $panel; + color: blue; + } + """ + + def compose(self) -> ComposeResult: + yield Canvas(30, 30, Color.parse("cornflowerblue")) + + def on_mount(self) -> None: + self.query_one(Canvas).draw_rectangle(2, 2, 26, 26) + + +if __name__ == "__main__": + DrawRectangleApp().run() diff --git a/docs/examples/mandelbrot.py b/docs/examples/mandelbrot.py new file mode 100644 index 0000000..91e71b4 --- /dev/null +++ b/docs/examples/mandelbrot.py @@ -0,0 +1,67 @@ +from typing import Iterator + +from textual.app import App, ComposeResult +from textual.color import Color + +from textual_canvas.canvas import Canvas + +BLUE_BROWN = [ + Color(66, 30, 15), + Color(25, 7, 26), + Color(9, 1, 47), + Color(4, 4, 73), + Color(0, 7, 100), + Color(12, 44, 138), + Color(24, 82, 177), + Color(57, 125, 209), + Color(134, 181, 229), + Color(211, 236, 248), + Color(241, 233, 191), + Color(248, 201, 95), + Color(255, 170, 0), + Color(204, 128, 0), + Color(153, 87, 0), + Color(106, 52, 3), +] +"""https://stackoverflow.com/a/16505538/2123348""" + + +def mandelbrot(x: float, y: float) -> int: + c1 = complex(x, y) + c2 = 0j + for n in range(40): + if abs(c2) > 2: + return n + c2 = c1 + (c2**2.0) + return 0 + + +def frange(r_from: float, r_to: float, size: int) -> Iterator[float]: + steps = 0 + step = (r_to - r_from) / size + n = r_from + while n < r_to and steps < size: + yield n + n += step + steps += 1 + + +class MandelbrotApp(App[None]): + def compose(self) -> ComposeResult: + yield Canvas(120, 90) + + def on_mount(self) -> None: + canvas = self.query_one(Canvas) + for x_pixel, x_point in enumerate(frange(-2.5, 1.5, canvas.width)): + for y_pixel, y_point in enumerate(frange(-1.5, 1.5, canvas.height)): + canvas.set_pixel( + x_pixel, + y_pixel, + BLUE_BROWN[value % 16] + if (value := mandelbrot(x_point, y_point)) + else Color(0, 0, 0), + ) + + +if __name__ == "__main__": + MandelbrotApp().run() diff --git a/docs/examples/own_background.py b/docs/examples/own_background.py new file mode 100644 index 0000000..5cfea22 --- /dev/null +++ b/docs/examples/own_background.py @@ -0,0 +1,20 @@ +from textual.app import App, ComposeResult +from textual.color import Color + +from textual_canvas import Canvas + + +class OwnBackgroundApp(App[None]): + CSS = """ + Canvas { + border: solid black; + background: cornflowerblue; + } + """ + + def compose(self) -> ComposeResult: + yield Canvas(30, 30, Color(80, 80, 255)) # (1)! + + +if __name__ == "__main__": + OwnBackgroundApp().run() diff --git a/docs/examples/set_pixel.py b/docs/examples/set_pixel.py new file mode 100644 index 0000000..7368803 --- /dev/null +++ b/docs/examples/set_pixel.py @@ -0,0 +1,23 @@ +from textual.app import App, ComposeResult +from textual.color import Color + +from textual_canvas import Canvas + + +class SetPixelApp(App[None]): + CSS = """ + Canvas { + background: $panel; + color: red; + } + """ + + def compose(self) -> ComposeResult: + yield Canvas(30, 30, Color.parse("cornflowerblue")) + + def on_mount(self) -> None: + self.query_one(Canvas).set_pixel(10, 10) + + +if __name__ == "__main__": + SetPixelApp().run() diff --git a/docs/examples/set_pixel_colour.py b/docs/examples/set_pixel_colour.py new file mode 100644 index 0000000..ffa7147 --- /dev/null +++ b/docs/examples/set_pixel_colour.py @@ -0,0 +1,27 @@ +from textual.app import App, ComposeResult +from textual.color import Color + +from textual_canvas import Canvas + + +class SetPixelApp(App[None]): + CSS = """ + Canvas { + background: $panel; + } + """ + + def compose(self) -> ComposeResult: + yield Canvas(30, 30, Color.parse("cornflowerblue")) + + def on_mount(self) -> None: + for offset, colour in enumerate(("red", "green", "blue")): + self.query_one(Canvas).set_pixel( + 10 + offset, + 10 + offset, + Color.parse(colour), + ) + + +if __name__ == "__main__": + SetPixelApp().run() diff --git a/docs/examples/set_pixels.py b/docs/examples/set_pixels.py new file mode 100644 index 0000000..c791a31 --- /dev/null +++ b/docs/examples/set_pixels.py @@ -0,0 +1,30 @@ +from textual.app import App, ComposeResult +from textual.color import Color + +from textual_canvas import Canvas + + +class SetPixelsApp(App[None]): + CSS = """ + Canvas { + background: $panel; + color: red; + } + """ + + def compose(self) -> ComposeResult: + yield Canvas(30, 30, Color.parse("cornflowerblue")) + + def on_mount(self) -> None: + self.query_one(Canvas).set_pixels( + ( + (10, 10), + (15, 15), + (10, 15), + (15, 10), + ) + ) + + +if __name__ == "__main__": + SetPixelsApp().run() diff --git a/docs/examples/sizing.py b/docs/examples/sizing.py new file mode 100644 index 0000000..9f9e7cd --- /dev/null +++ b/docs/examples/sizing.py @@ -0,0 +1,31 @@ +from textual.app import App, ComposeResult +from textual.color import Color + +from textual_canvas import Canvas + + +class CanvasSizingApp(App[None]): + CSS = """ + Screen { + layout: horizontal; + } + + Canvas { + background: $panel; + border: solid cornflowerblue; + width: 1fr; + height: 1fr; + } + """ + + def compose(self) -> ComposeResult: + yield Canvas(20, 20, Color(128, 0, 128), id="smaller") + yield Canvas(60, 60, Color(128, 0, 128), id="bigger") + + def on_mount(self) -> None: + self.query_one("#smaller").border_title = "Widget > Canvas" + self.query_one("#bigger").border_title = "Canvas > Widget" + + +if __name__ == "__main__": + CanvasSizingApp().run() diff --git a/docs/source/CNAME b/docs/source/CNAME new file mode 100644 index 0000000..16b08ed --- /dev/null +++ b/docs/source/CNAME @@ -0,0 +1 @@ +textual-canvas.davep.dev diff --git a/docs/source/canvas.md b/docs/source/canvas.md new file mode 100644 index 0000000..cc8bd97 --- /dev/null +++ b/docs/source/canvas.md @@ -0,0 +1,7 @@ +--- +title: textual_canvas.canvas +--- + +::: textual_canvas.canvas + +[//]: # (canvas.md ends here) diff --git a/docs/source/changelog.md b/docs/source/changelog.md new file mode 100644 index 0000000..79b8fcc --- /dev/null +++ b/docs/source/changelog.md @@ -0,0 +1,3 @@ +--8<-- "ChangeLog.md" + +[//]: # (changelog.md ends here) diff --git a/docs/source/guide.md b/docs/source/guide.md new file mode 100644 index 0000000..b1a9d12 --- /dev/null +++ b/docs/source/guide.md @@ -0,0 +1,233 @@ +# Usage Guide + +## The `Canvas` widget + +The [`Canvas`][textual_canvas.canvas.Canvas] widget is used like any other +Textual widget; it is imported as: + +```python +from textual_canvas import Canvas +``` + +and then can be [mounted][textual.screen.Screen.mount] or +[composed][textual.screen.Screen.compose] like any other widget. + +### Sizing + +When [creating it][textual_canvas.canvas.Canvas] you provide a +width and a height of the canvas in "pixels" Note that these values are the +dimensions of the canvas that the "pixels" are drawn on, not the size of the +widget; the widget itself is sized using all the normal Textual styling and +geometry rules. + +To illustrate, here are two `Canvas` widgets, one where the widget is bigger +than the canvas, and one where the canvas is bigger than the widget: + +=== "Widget vs canvas sizing" + + ```{.textual path="docs/examples/sizing.py" lines=20 columns=80} + ``` + +=== "sizing.py" + + ```python + --8<-- "docs/examples/sizing.py" + ``` + +Note how the `Canvas` widget on the left is bigger than the canvas it is +displaying; whereas the widget on the right is smaller than its canvas so it +has scrollbars. + +### Colours + +There are three main colours to consider when working with `Canvas`: + +- The widget background colour. +- The canvas background colour. +- The current "pen" colour. + +#### Widget vs canvas background + +The difference ion the first two items listed above might not seem obvious +to start with. The `Canvas` widget, like all other Textual widgets, has a +[background](https://textual.textualize.io/styles/background/); you can +style this with CSS just as you always would. But the canvas itself -- the +area that you'll be drawing in inside the widget -- can have its own +background colour. + +By default the canvas background colour will be set to the widget's +background colour; but you can pass +[`canvas_color`][textual_canvas.canvas.Canvas] as a parameter to change +this. + +To illustrate, here is a `Canvas` widget where no background colour is +specified, so the canvas background and the widget background are the same: + +=== "30x30 canvas with widget's background" + + ```{.textual path="docs/examples/default_background.py"} + ``` + +=== "default_background.py" + + ```python + --8<-- "docs/examples/default_background.py" + ``` + + 1. The `Canvas` is created without a given colour, so the widget's + `background` will be used as the canvas background colour. + +Note how the user won't be able to see what's canvas background and what's +widget outside of the background. + +On the other hand, if we take the same code and give the `Canvas` its own +background colour when we create it: + +=== "30x30 canvas with its own background" + + ```{.textual path="docs/examples/own_background.py"} + ``` + +=== "own_background.py" + + ```python hl_lines="16" + --8<-- "docs/examples/own_background.py" + ``` + + 1. Note how `Canvas` is given its own background colour. + +!!! note + + The defaulting of the canvas background to the widget's background is + something that only happens when the widget is mounted. If you style the + widget's background differently later on, the canvas' background **will + not change accordingly**. + +#### The pen colour + +The `Canvas` widget has a "pen" colour; any time a drawing operation is +performed, if no colour is given to the method, the "pen" colour is used. By +default that colour is taken from the +[`color`](https://textual.textualize.io/styles/color/) styling of the +widget. + +!!! note + + The defaulting of the pen colour to the widget's colour is + something that only happens when the widget is mounted. If you style the + widget's `color` differently later on, the canvas' pen colour **will + not change accordingly**. + +## Drawing on the canvas + +The canvas widget provides a number of methods for drawing on it. + +!!! note + + All coordinates used when drawing are relative to the top left corner of + the canvas. + +### Drawing a single pixel + +Use [`set_pixel`][textual_canvas.canvas.Canvas.set_pixel] to set the colour +of a single pixel on the canvas. For example: + +=== "Drawing a single pixel" + + ```{.textual path="docs/examples/set_pixel.py"} + ``` + +=== "set_pixel.py" + + ```python + --8<-- "docs/examples/set_pixel.py" + ``` + +That example is using the default pen colour, which in turn is defaulting +the widget's [`color`](https://textual.textualize.io/styles/color/). Instead +we can set pixels to specific colours: + +=== "Drawing pixels with specific colours" + + ```{.textual path="docs/examples/set_pixel_colour.py"} + ``` + +=== "set_pixel_colour.py" + + ```python + --8<-- "docs/examples/set_pixel_colour.py" + ``` +### Drawing multiple pixels + +Use [`set_pixels`][textual_canvas.canvas.Canvas.set_pixels] to draw multiple +pixels of the same colour at once. For example: + +=== "Drawing multiple pixels" + + ```{.textual path="docs/examples/set_pixels.py"} + ``` + +=== "set_pixels.py" + + ```python + --8<-- "docs/examples/set_pixels.py" + ``` + +### Drawing a line + +Use [`draw_line`][textual_canvas.canvas.Canvas.draw_line] to draw a line on +the canvas. For example: + +=== "Drawing a line" + + ```{.textual path="docs/examples/draw_line.py"} + ``` + +=== "draw_line.py" + + ```python + --8<-- "docs/examples/draw_line.py" + ``` + +### Drawing a rectangle + +Use [`draw_rectangle`][textual_canvas.canvas.Canvas.draw_rectangle] to draw +a rectangle on the canvas. For example: + +=== "Drawing a rectangle" + + ```{.textual path="docs/examples/draw_rectangle.py"} + ``` + +=== "draw_rectangle.py" + + ```python + --8<-- "docs/examples/draw_rectangle.py" + ``` + +### Drawing a circle + +Use [`draw_circle`][textual_canvas.canvas.Canvas.draw_circle] to draw a +circle on the canvas. For example: + +=== "Drawing a circle" + + ```{.textual path="docs/examples/draw_circle.py"} + ``` + +=== "draw_circle.py" + + ```python + --8<-- "docs/examples/draw_circle.py" + ``` + +## Further help + +You can find more detailed documentation of the API [in the next +section](canvas.md). If you still have questions or have ideas for +improvements please feel free to chat to me [in GitHub +discussions](https://github.com/davep/textual-canvas/discussions); if you +think you've found a problem, please feel free to [raise an +issue](https://github.com/davep/textual-canvas/issues). + +[//]: # (guide.md ends here) diff --git a/docs/source/index.md b/docs/source/index.md new file mode 100644 index 0000000..0d315ca --- /dev/null +++ b/docs/source/index.md @@ -0,0 +1,49 @@ +# Introduction + +`textual-canvas` provides a simple terminal-based drawing canvas widget for +use with [Textual](https://textual.textualize.io/). Initially developed as a +widget for building +[`textual-mandelbrot`](https://github.com/davep/textual-mandelbrot), it made +sense to spin it out into its own general-purpose library. + +=== "Textual Canvas Example" + + ```{.textual path="docs/examples/mandelbrot.py" lines=45 columns=120} + ``` + +=== "mandelbrot.py" + + ```py + --8<-- "docs/examples/mandelbrot.py" + ``` + +The widget is based around the use of half block characters; this has two +main advantages: + +- The "pixels" are generally nice and square in most terminal setups. +- You get to use the [full range of + colours](https://textual.textualize.io/api/color/) for each pixel. + +## Installing + +`textual-canvas` is [available from pypi](https://pypi.org/project/textual-canvas/) +and can be installed with `pip` or similar Python package tools: + +```shell +pip install textual-canvas +``` + +## Requirements + +The only requirements for this library, other than the standard Python +library, are: + +- [`textual`](https://textual.textualize.io/) (obviously) +- [`typing-extensions`](https://typing-extensions.readthedocs.io/en/latest/#). + +## Supported Python versions + +`textual-canvas` is usable with [all supported Python +versions](https://devguide.python.org/versions/) from 3.9 and above. + +[//]: # (index.md ends here) diff --git a/docs/source/licence.md b/docs/source/licence.md new file mode 100644 index 0000000..c219b00 --- /dev/null +++ b/docs/source/licence.md @@ -0,0 +1,5 @@ +``` +--8<-- "LICENSE" +``` + +[//]: # (licence.md ends here) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..dbbe425 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,103 @@ +site_name: textual-canvas +docs_dir: docs/source +repo_url: https://github.com/davep/textual-canvas + +nav: + - Guide: + - index.md + - guide.md + - Library Contents: + - canvas.md + - Change Log: changelog.md + - Licence: licence.md + +watch: + - src/textual_canvas + - docs/examples + +markdown_extensions: + - admonition + - pymdownx.snippets + - markdown.extensions.attr_list + - pymdownx.superfences: + custom_fences: + - name: textual + class: textual + format: !!python/name:textual._doc.format_svg + - pymdownx.tabbed: + alternate_style: true + +plugins: + search: + autorefs: + mkdocstrings: + default_handler: python + enable_inventory: true + handlers: + python: + inventories: + - https://docs.python.org/3/objects.inv + - https://textual.textualize.io/objects.inv + options: + filters: + - "!^_" + - "^__.+__$" + - "!^on_mount$" + - "!^compose$" + - "!^render_line" + modernize_annotations: false + show_symbol_type_heading: true + show_symbol_type_toc: true + show_signature_annotations: false + separate_signature: true + signature_crossrefs: true + merge_init_into_class: true + parameter_headings: true + show_root_heading: false + docstring_options: + ignore_init_summary: true + show_source: false + +theme: + name: material + icon: + logo: fontawesome/solid/paintbrush + features: + - navigation.tabs + - navigation.indexes + - navigation.tabs.sticky + - navigation.footer + - content.code.annotate + - content.code.copy + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + accent: purple + toggle: + icon: material/weather-sunny + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + toggle: + icon: material/weather-night + name: Switch to light mode + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/davep + - icon: fontawesome/brands/python + link: https://pypi.org/user/davepearson/ + - icon: fontawesome/brands/mastodon + link: https://fosstodon.org/@davep + - icon: fontawesome/brands/bluesky + link: https://bsky.app/profile/davep.org + - icon: fontawesome/brands/threads + link: https://www.threads.net/@davepdotorg + - icon: fontawesome/brands/youtube + link: https://www.youtube.com/@DavePearson + - icon: fontawesome/brands/steam + link: https://steamcommunity.com/id/davepdotorg + +### mkdocs.yml ends here diff --git a/pyproject.toml b/pyproject.toml index c1d9ac1..c56a84e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,9 +35,9 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/davep/textual-canvas" +Homepage = "https://textual-canvas.davep.dev/" Repository = "https://github.com/davep/textual-canvas" -Documentation = "https://github.com/davep/textual-canvas" +Documentation = "https://textual-canvas.davep.dev/" Source = "https://github.com/davep/textual-canvas" Issues = "https://github.com/davep/textual-canvas/issues" Discussions = "https://github.com/davep/textual-canvas/discussions" @@ -55,6 +55,8 @@ dev-dependencies = [ "mypy>=1.15.0", "ruff>=0.11.5", "codespell>=2.4.1", + "mkdocs-material>=9.6.11", + "mkdocstrings[python]>=0.29.1", ] [tool.hatch.metadata] diff --git a/requirements-dev.lock b/requirements-dev.lock index 8283675..a40ed43 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -10,41 +10,117 @@ # universal: false -e file:. +babel==2.17.0 + # via mkdocs-material +backrefs==5.8 + # via mkdocs-material +certifi==2025.1.31 + # via requests cfgv==3.4.0 # via pre-commit +charset-normalizer==3.4.1 + # via requests +click==8.1.8 + # via mkdocs codespell==2.4.1 +colorama==0.4.6 + # via griffe + # via mkdocs-material distlib==0.3.9 # via virtualenv filelock==3.18.0 # via virtualenv +ghp-import==2.1.0 + # via mkdocs +griffe==1.7.2 + # via mkdocstrings-python identify==2.6.9 # via pre-commit +idna==3.10 + # via requests +jinja2==3.1.6 + # via mkdocs + # via mkdocs-material + # via mkdocstrings linkify-it-py==2.0.3 # via markdown-it-py +markdown==3.8 + # via mkdocs + # via mkdocs-autorefs + # via mkdocs-material + # via mkdocstrings + # via pymdown-extensions markdown-it-py==3.0.0 # via mdit-py-plugins # via rich # via textual +markupsafe==3.0.2 + # via jinja2 + # via mkdocs + # via mkdocs-autorefs + # via mkdocstrings mdit-py-plugins==0.4.2 # via markdown-it-py mdurl==0.1.2 # via markdown-it-py +mergedeep==1.3.4 + # via mkdocs + # via mkdocs-get-deps +mkdocs==1.6.1 + # via mkdocs-autorefs + # via mkdocs-material + # via mkdocstrings +mkdocs-autorefs==1.4.1 + # via mkdocstrings + # via mkdocstrings-python +mkdocs-get-deps==0.2.0 + # via mkdocs +mkdocs-material==9.6.11 +mkdocs-material-extensions==1.3.1 + # via mkdocs-material +mkdocstrings==0.29.1 + # via mkdocstrings-python +mkdocstrings-python==1.16.10 + # via mkdocstrings mypy==1.15.0 mypy-extensions==1.0.0 # via mypy nodeenv==1.9.1 # via pre-commit +packaging==24.2 + # via mkdocs +paginate==0.5.7 + # via mkdocs-material +pathspec==0.12.1 + # via mkdocs platformdirs==4.3.7 + # via mkdocs-get-deps # via textual # via virtualenv pre-commit==4.2.0 pygments==2.19.1 + # via mkdocs-material # via rich +pymdown-extensions==10.14.3 + # via mkdocs-material + # via mkdocstrings +python-dateutil==2.9.0.post0 + # via ghp-import pyyaml==6.0.2 + # via mkdocs + # via mkdocs-get-deps # via pre-commit + # via pymdown-extensions + # via pyyaml-env-tag +pyyaml-env-tag==0.1 + # via mkdocs +requests==2.32.3 + # via mkdocs-material rich==14.0.0 # via textual ruff==0.11.5 +six==1.17.0 + # via python-dateutil textual==3.1.0 # via textual-canvas typing-extensions==4.13.2 @@ -53,5 +129,9 @@ typing-extensions==4.13.2 # via textual-canvas uc-micro-py==1.0.3 # via linkify-it-py +urllib3==2.4.0 + # via requests virtualenv==20.30.0 # via pre-commit +watchdog==6.0.0 + # via mkdocs diff --git a/src/textual_canvas/canvas.py b/src/textual_canvas/canvas.py index 21e87e6..c0af2e0 100644 --- a/src/textual_canvas/canvas.py +++ b/src/textual_canvas/canvas.py @@ -29,7 +29,7 @@ ############################################################################## class CanvasError(Exception): - """Type of errors raised by the `Canvas` widget.""" + """Type of errors raised by the [`Canvas`][textual_canvas.canvas.Canvas] widget.""" ############################################################################## @@ -42,11 +42,6 @@ class Canvas(ScrollView, can_focus=True): whole cell as a simple pixel. The origin of the canvas is the top left corner. - - NOTE: At the moment this is coded in a very simple way, mainly to help - decide on the API it will make available (which I intend to be as simple - as possible). Little to no thought has been given to performance. First - I want to get it right, then I want to get it fast. """ def __init__( @@ -56,7 +51,7 @@ def __init__( canvas_color: Color | None = None, pen_color: Color | None = None, name: str | None = None, - id: str | None = None, # pylint:disable=redefined-builtin + id: str | None = None, classes: str | None = None, disabled: bool = False, ): @@ -147,9 +142,9 @@ def clear(self, color: Color | None = None) -> Self: Note: If the color isn't provided, then the color used when first - making the canvas is used, this in turn because the new default + making the canvas is used, this in turn becomes the new default color (and will then be used for subsequent clears, unless - another color is provided.) + another color is provided). """ if color is not None: self._canvas_colour = color @@ -168,7 +163,7 @@ def set_pen(self, color: Color | None) -> Self: Note: Setting the colour to [`None`][None] specifies that the widget's - currently-styled [`color`][textual.widget.Widget.styles.color] + currently-styled [`color`](https://textual.textualize.io/guide/styles/#styles-object) should be used. """ self._pen_colour = color