Name | textual-hires-canvas JSON |
Version |
0.10.0
JSON |
| download |
home_page | None |
Summary | High-resolution drawing canvas for Textual apps |
upload_time | 2025-07-14 11:12:13 |
maintainer | None |
docs_url | None |
author | None |
requires_python | >=3.10 |
license | None |
keywords |
|
VCS |
 |
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
# High-resolution drawing canvas for Textual apps
[Textual](https://www.textualize.io/) is an excellent Python framework for building applications in the terminal, or on the web. This library provides a canvas widget which your app can draw on using primitives like `set_pixel()`, `draw_line()` and `draw_rectangle_box()`. The canvas can also draw using _high-resolution_ characters like unicode half blocks, quadrants and 8-dot Braille characters. It may still be apparent that these are drawn using characters that take up a full block in the terminal, especially when lines cross. However, the use of these characters can reduce the line thickness and improve the resolution tremendously.
## Screenshots



## Running the demo / installation
If you have [uv](https://astral.sh/uv/) installed, run
```console
uvx textual-hires-canvas
```
If you use pipx, replace `uvx` with `pipx`. Alternatively, install the package with `pip` and run the demo:
```console
pip install textual-hires-canvas
python -m textual_hires_canvas.demo
```
## Tutorial
A simple example of using the canvas widget in your Textual app is given below.

```python
from textual.app import App, ComposeResult
from textual_hires_canvas import Canvas, HiResMode, TextAlign
class MinimalApp(App[None]):
def compose(self) -> ComposeResult:
yield Canvas(40, 20)
def on_mount(self) -> None:
canvas = self.query_one(Canvas)
canvas.draw_rectangle_box(0, 0, 39, 19, thickness=2)
canvas.draw_line(1, 1, 38, 18, style="green")
canvas.draw_hires_line(1, 18.5, 38.5, 1, HiResMode.BRAILLE, style="blue")
canvas.write_text(
20,
1,
"A [italic]simple[/] demo of the [bold yellow]Canvas[/]",
TextAlign.CENTER,
)
if __name__ == "__main__":
MinimalApp().run()
```
Here, the `Canvas` widget is initialised with size 40 by 20 and a rectangular box, a line, a high-resolution line and some text is displayed. Coordinates are given in (x, y) fashion where (0, 0) is the top-left corner of the widget. The `draw_line()` method accepts a `char` argument which you can pass any unicode character you'd like to draw in the terminal. The `style` argument accepts Textual/Rich styles like `green` or `yellow on blue`. The `HiresMode`s are `HALFBLOCK`, `QUADRANT` and `BRAILLE`.
### Resizing the canvas
To automatically resize the Canvas to fit the available space in your app or the terminal, you can handle the `Canvas.Resize` event and call `Canvas.reset(size=event.size)` to resize the canvas. Be aware that the canvas is cleared and you have to redraw, like this:

```python
from textual import on
from textual.app import App, ComposeResult
from textual_hires_canvas import Canvas, HiResMode, TextAlign
class MinimalApp(App[None]):
def compose(self) -> ComposeResult:
yield Canvas()
@on(Canvas.Resize)
def draw(self, event: Canvas.Resize):
canvas = event.canvas
size = event.size
canvas.reset(size=event.size)
canvas.draw_rectangle_box(0, 0, size.width - 1, size.height - 1, thickness=2)
canvas.draw_line(1, 1, size.width - 2, size.height - 2, style="green")
canvas.draw_hires_line(
1, size.height - 1.5, size.width - 1.5, 1, HiResMode.BRAILLE, style="blue"
)
canvas.write_text(
size.width // 2,
1,
"A [italic]simple[/] demo of the [bold yellow]Canvas[/]",
TextAlign.CENTER,
)
if __name__ == "__main__":
MinimalApp().run()
```
### The full demo code
Finally, the code of the demo is given below, showing how you can handle simple animations:
```python
from math import floor
from textual import on
from textual.app import App, ComposeResult
from textual_hires_canvas import Canvas, HiResMode
class DemoApp(App[None]):
_box_x_pos = 0
_box_y_pos = 0
_text_x_pos = 0.0
_box_x_step = 1
_box_y_step = 1
_text_x_step = 0.5
def compose(self) -> ComposeResult:
yield Canvas(1, 1)
def on_mount(self) -> None:
self.set_interval(1 / 10, self.redraw_canvas)
@on(Canvas.Resize)
def resize(self, event: Canvas.Resize) -> None:
event.canvas.reset(size=event.size)
def redraw_canvas(self) -> None:
canvas = self.query_one(Canvas)
canvas.reset()
canvas.draw_hires_line(2, 10, 78, 2, hires_mode=HiResMode.BRAILLE, style="blue")
canvas.draw_hires_line(2, 5, 78, 10, hires_mode=HiResMode.BRAILLE)
canvas.draw_line(0, 0, 8, 8)
canvas.draw_line(0, 19, 39, 0, char="X", style="red")
canvas.write_text(
floor(self._text_x_pos),
10,
"[green]This text is [bold]easy[/bold] to read",
)
canvas.draw_rectangle_box(
self._box_x_pos,
self._box_y_pos,
self._box_x_pos + 20,
self._box_y_pos + 10,
thickness=2,
)
self._box_x_pos += self._box_x_step
if (self._box_x_pos <= 0) or (self._box_x_pos + 20 >= canvas.size.width - 1):
self._box_x_step *= -1
self._box_y_pos += self._box_y_step
if (self._box_y_pos <= 0) or (self._box_y_pos + 10 >= canvas.size.height - 1):
self._box_y_step *= -1
self._text_x_pos += self._text_x_step
if self._text_x_pos >= canvas.size.width + 20:
self._text_x_pos = -20
def main():
DemoApp().run()
if __name__ == "__main__":
main()
```
## List of canvas methods
- `reset()` or `reset(size)`: clear the canvas.
- `get_pixel(x, y)`: get character at pixel coordinages.
- `set_pixel(x, y, char, style)`: set a character at pixel coordinates.
- `set_pixels(coordinates, char, style)`: set multiple pixels.
- `set_hires_pixels(coordinates, hires_mode, style)`: set high-resolution pixels.
- `draw_line(x0, y0, x1, y1, char, style)`: draw a line consisting of specific characters.
- `draw_lines(coordinates, char, style)`: draw multiple lines.
- `draw_hires_line(x0, y0, x1, y1, hires_mode, style)`: draw a high-resolution line using a particular mode.
- `draw_hires_lines(coordinates, hires_mode, style)`: draw multiple high-resolution lines.
- `draw_rectangle_box(x0, y0, x1, y1, thickness, style)`: draw a rectangle using box-drawing characters.
## Alternatives
[Textual-canvas](https://github.com/davep/textual-canvas) by Dave Pearson is much better suited to display a large bitmap image with a scrollable viewport. It uses half-block characters to create square pixels.
Raw data
{
"_id": null,
"home_page": null,
"name": "textual-hires-canvas",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": null,
"keywords": null,
"author": null,
"author_email": "David Fokkema <davidfokkema@icloud.com>",
"download_url": "https://files.pythonhosted.org/packages/b1/dc/c2e31337c726bf12a57df785977ba6c80599a5c6060eab3551c84d3382df/textual_hires_canvas-0.10.0.tar.gz",
"platform": null,
"description": "# High-resolution drawing canvas for Textual apps\n\n[Textual](https://www.textualize.io/) is an excellent Python framework for building applications in the terminal, or on the web. This library provides a canvas widget which your app can draw on using primitives like `set_pixel()`, `draw_line()` and `draw_rectangle_box()`. The canvas can also draw using _high-resolution_ characters like unicode half blocks, quadrants and 8-dot Braille characters. It may still be apparent that these are drawn using characters that take up a full block in the terminal, especially when lines cross. However, the use of these characters can reduce the line thickness and improve the resolution tremendously.\n\n## Screenshots\n\n\n\n\n\n\n\n## Running the demo / installation\n\nIf you have [uv](https://astral.sh/uv/) installed, run\n```console\nuvx textual-hires-canvas\n```\nIf you use pipx, replace `uvx` with `pipx`. Alternatively, install the package with `pip` and run the demo:\n```console\npip install textual-hires-canvas\npython -m textual_hires_canvas.demo\n```\n\n## Tutorial\n\nA simple example of using the canvas widget in your Textual app is given below.\n\n```python\nfrom textual.app import App, ComposeResult\n\nfrom textual_hires_canvas import Canvas, HiResMode, TextAlign\n\n\nclass MinimalApp(App[None]):\n def compose(self) -> ComposeResult:\n yield Canvas(40, 20)\n\n def on_mount(self) -> None:\n canvas = self.query_one(Canvas)\n canvas.draw_rectangle_box(0, 0, 39, 19, thickness=2)\n canvas.draw_line(1, 1, 38, 18, style=\"green\")\n canvas.draw_hires_line(1, 18.5, 38.5, 1, HiResMode.BRAILLE, style=\"blue\")\n canvas.write_text(\n 20,\n 1,\n \"A [italic]simple[/] demo of the [bold yellow]Canvas[/]\",\n TextAlign.CENTER,\n )\n\n\nif __name__ == \"__main__\":\n MinimalApp().run()\n```\nHere, the `Canvas` widget is initialised with size 40 by 20 and a rectangular box, a line, a high-resolution line and some text is displayed. Coordinates are given in (x, y) fashion where (0, 0) is the top-left corner of the widget. The `draw_line()` method accepts a `char` argument which you can pass any unicode character you'd like to draw in the terminal. The `style` argument accepts Textual/Rich styles like `green` or `yellow on blue`. The `HiresMode`s are `HALFBLOCK`, `QUADRANT` and `BRAILLE`.\n\n### Resizing the canvas\n\nTo automatically resize the Canvas to fit the available space in your app or the terminal, you can handle the `Canvas.Resize` event and call `Canvas.reset(size=event.size)` to resize the canvas. Be aware that the canvas is cleared and you have to redraw, like this:\n\n```python\nfrom textual import on\nfrom textual.app import App, ComposeResult\n\nfrom textual_hires_canvas import Canvas, HiResMode, TextAlign\n\n\nclass MinimalApp(App[None]):\n def compose(self) -> ComposeResult:\n yield Canvas()\n\n @on(Canvas.Resize)\n def draw(self, event: Canvas.Resize):\n canvas = event.canvas\n size = event.size\n canvas.reset(size=event.size)\n\n canvas.draw_rectangle_box(0, 0, size.width - 1, size.height - 1, thickness=2)\n canvas.draw_line(1, 1, size.width - 2, size.height - 2, style=\"green\")\n canvas.draw_hires_line(\n 1, size.height - 1.5, size.width - 1.5, 1, HiResMode.BRAILLE, style=\"blue\"\n )\n canvas.write_text(\n size.width // 2,\n 1,\n \"A [italic]simple[/] demo of the [bold yellow]Canvas[/]\",\n TextAlign.CENTER,\n )\n\n\nif __name__ == \"__main__\":\n MinimalApp().run()\n```\n\n### The full demo code\n\nFinally, the code of the demo is given below, showing how you can handle simple animations:\n```python\nfrom math import floor\n\nfrom textual import on\nfrom textual.app import App, ComposeResult\n\nfrom textual_hires_canvas import Canvas, HiResMode\n\n\nclass DemoApp(App[None]):\n _box_x_pos = 0\n _box_y_pos = 0\n _text_x_pos = 0.0\n\n _box_x_step = 1\n _box_y_step = 1\n _text_x_step = 0.5\n\n def compose(self) -> ComposeResult:\n yield Canvas(1, 1)\n\n def on_mount(self) -> None:\n self.set_interval(1 / 10, self.redraw_canvas)\n\n @on(Canvas.Resize)\n def resize(self, event: Canvas.Resize) -> None:\n event.canvas.reset(size=event.size)\n\n def redraw_canvas(self) -> None:\n canvas = self.query_one(Canvas)\n canvas.reset()\n canvas.draw_hires_line(2, 10, 78, 2, hires_mode=HiResMode.BRAILLE, style=\"blue\")\n canvas.draw_hires_line(2, 5, 78, 10, hires_mode=HiResMode.BRAILLE)\n canvas.draw_line(0, 0, 8, 8)\n canvas.draw_line(0, 19, 39, 0, char=\"X\", style=\"red\")\n canvas.write_text(\n floor(self._text_x_pos),\n 10,\n \"[green]This text is [bold]easy[/bold] to read\",\n )\n canvas.draw_rectangle_box(\n self._box_x_pos,\n self._box_y_pos,\n self._box_x_pos + 20,\n self._box_y_pos + 10,\n thickness=2,\n )\n self._box_x_pos += self._box_x_step\n if (self._box_x_pos <= 0) or (self._box_x_pos + 20 >= canvas.size.width - 1):\n self._box_x_step *= -1\n self._box_y_pos += self._box_y_step\n if (self._box_y_pos <= 0) or (self._box_y_pos + 10 >= canvas.size.height - 1):\n self._box_y_step *= -1\n self._text_x_pos += self._text_x_step\n if self._text_x_pos >= canvas.size.width + 20:\n self._text_x_pos = -20\n\n\ndef main():\n DemoApp().run()\n\n\nif __name__ == \"__main__\":\n main()\n```\n\n## List of canvas methods\n\n- `reset()` or `reset(size)`: clear the canvas.\n- `get_pixel(x, y)`: get character at pixel coordinages.\n- `set_pixel(x, y, char, style)`: set a character at pixel coordinates.\n- `set_pixels(coordinates, char, style)`: set multiple pixels.\n- `set_hires_pixels(coordinates, hires_mode, style)`: set high-resolution pixels.\n- `draw_line(x0, y0, x1, y1, char, style)`: draw a line consisting of specific characters.\n- `draw_lines(coordinates, char, style)`: draw multiple lines.\n- `draw_hires_line(x0, y0, x1, y1, hires_mode, style)`: draw a high-resolution line using a particular mode.\n- `draw_hires_lines(coordinates, hires_mode, style)`: draw multiple high-resolution lines. \n- `draw_rectangle_box(x0, y0, x1, y1, thickness, style)`: draw a rectangle using box-drawing characters.\n\n## Alternatives\n\n[Textual-canvas](https://github.com/davep/textual-canvas) by Dave Pearson is much better suited to display a large bitmap image with a scrollable viewport. It uses half-block characters to create square pixels.\n",
"bugtrack_url": null,
"license": null,
"summary": "High-resolution drawing canvas for Textual apps",
"version": "0.10.0",
"project_urls": {
"Changelog": "https://github.com/davidfokkema/textual-hires-canvas/blob/main/CHANGELOG.md",
"Homepage": "https://github.com/davidfokkema/textual-hires-canvas",
"Issues": "https://github.com/davidfokkema/textual-hires-canvas/issues",
"Repository": "https://github.com/davidfokkema/textual-hires-canvas.git"
},
"split_keywords": [],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "eaf1649b7b0f014cc8df6cd64fb23015e7b31ac3a4a814f571623cf71d3871ce",
"md5": "561f04248173f5a96699d311000f8946",
"sha256": "0cd9d815b7bd18eefc30c4626a03a297fbe1fe8ea959792bb9d953c5750520dd"
},
"downloads": -1,
"filename": "textual_hires_canvas-0.10.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "561f04248173f5a96699d311000f8946",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 17660,
"upload_time": "2025-07-14T11:12:12",
"upload_time_iso_8601": "2025-07-14T11:12:12.115972Z",
"url": "https://files.pythonhosted.org/packages/ea/f1/649b7b0f014cc8df6cd64fb23015e7b31ac3a4a814f571623cf71d3871ce/textual_hires_canvas-0.10.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "b1dcc2e31337c726bf12a57df785977ba6c80599a5c6060eab3551c84d3382df",
"md5": "3b51b3fd687330217575bba577b0670c",
"sha256": "d355fbd6bf4ff33e594ea0768911482db171272385e73be36da3748428a0b74f"
},
"downloads": -1,
"filename": "textual_hires_canvas-0.10.0.tar.gz",
"has_sig": false,
"md5_digest": "3b51b3fd687330217575bba577b0670c",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 1214111,
"upload_time": "2025-07-14T11:12:13",
"upload_time_iso_8601": "2025-07-14T11:12:13.900284Z",
"url": "https://files.pythonhosted.org/packages/b1/dc/c2e31337c726bf12a57df785977ba6c80599a5c6060eab3551c84d3382df/textual_hires_canvas-0.10.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-07-14 11:12:13",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "davidfokkema",
"github_project": "textual-hires-canvas",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "textual-hires-canvas"
}