Skip to content

Commit

Permalink
Enormous improvement to container logic with inner/outer containers.
Browse files Browse the repository at this point in the history
Fixed up docstrings in numerous places
Updated readme to reflect changes in usage
  • Loading branch information
edward-jazzhands committed Oct 26, 2024
1 parent c00befa commit e5d31e1
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 121 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# 2024-10-## 0.3.6
- Fixed up docstrings
# 2024-10-26 0.4.0
- Enormous improvement to container logic with inner/outer containers.
- Fixed up docstrings in numerous places
- Updated readme to reflect changes in usage

# 2024-10-25 0.3.5
- Fixed dependency problems in pyproject.toml
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ help:
@echo " make publish Publish the package, builds first"
@echo " make del-env Delete the virtual environment"

.PHONY: install install-full activate run run-demo run-dev clean build publish del-env
.PHONY: install install-full activate run run-demo run-dev console clean build publish del-env

install:
poetry install
Expand All @@ -38,6 +38,11 @@ run-demo:
run-dev:
textual run --dev textual_pyfiglet.__main__:PyFigletDemo

# I turn off events because its just too much output
# Note: requires dev tools.
console:
textual console -x EVENT

clean:
rm -rf build dist
find . -name "*.pyc" -delete
Expand Down
32 changes: 5 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Textual-PyFiglet is an implementation of [PyFiglet](https://github.com/pwaller/p

It provides a `FigletWidget` which is designed to be easy to use inside of Textual.

![Demo GIF](demo.gif)
![Demo GIF](https://raw.githubusercontent.com/edward-jazzhands/textual-pyfiglet/refs/heads/main/demo.gif)

# Key features

Expand All @@ -47,9 +47,7 @@ PyFiglet wheel: **1.1 MB**. --> Textual-PyFiglet wheel: **71 KB**.

The widget is based on `Static` and is designed to mimick its behavior. That means it can drop-in replace any Static widget, and it should just work without even adding or changing arguments (using default font). Assuming you're accounting for the size of the text somehow.

It achieves this by simply overriding the `update()` method in Static. When update is called, PyFiglet will convert the input text, and PyFiglet's output is passed to `self.renderable`.

By default, the FigletWidget will automatically set its own size when it updates. (width and height are set to auto). But, **it will also respect any container or widget it's inside of, and wrap the text accordingly.**
You can dynamically set the size (ie 1fr, 100%) as you would with any Textual widget. It will respond automatically to any widget resize events, and re-draw the figlet.

### Real-time updating:

Expand Down Expand Up @@ -107,30 +105,10 @@ yield FigletWidget("Label of Things", id="figlet1" font="small")

## Resizing

The FigletWidget will try to wrap to whatever parent container it is inside of. If you want to contain it to an area, it's best to place it inside a container:
The FigletWidget will auto-update the rendering area whenever it gets resized.
Internally it uses Textual's `on_resize` method. So it should work automatically.
Just set the widget to the size you want, and PyFiglet will render what it can in that space.

```python
with Container(id="figlet_container1", classes="figlet_labels"):
yield FigletWidget("Label of Things", id="figlet1")
```

You can resize the container, and then trigger the FigletWidget update method:

```python
def resize_container(self, width: int):
self.query("#figlet_container1").styles.width = width
self.query("#figlet1").update(resized=True)
```
Note that whenever you are updating to resize the widget's render area, you must use the `resized=True` argument.

Likewise you can set it to resize when the screen size changes by calling it from the main app. This is assuming it's inside a container which would be affected by this.

```python
class MyTextualApp(App):

def on_resize(self):
self.query("#figlet1").update(resized=True)
```
## Change the font:

The widget will update automatically when this is run:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual-pyfiglet"
version = "0.3.6"
version = "0.4.0"
description = "A Widget implementation of PyFiglet for Textual"
authors = ["edward-jazzhands <ed.jazzhands@gmail.com>"]
license = "MIT"
Expand Down
1 change: 1 addition & 0 deletions tests/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
These are original PyFiglet tests. Nothing yet written by Textual-Pyfiglet.
30 changes: 7 additions & 23 deletions textual_pyfiglet/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ def compose(self):
yield Button("Set", id="set_button")

with VerticalScroll(id="main_window"):
with Container(id="figlet_container"):
yield FigletWidget(id="figlet_widget")
yield FigletWidget("Starter Text", id="figlet_widget")

with Container(id="bottom_bar"):
with Horizontal():
Expand All @@ -96,8 +95,7 @@ def compose(self):

def on_mount(self):

self.figlet_widget = cast(FigletWidget, self.query_one("#figlet_widget"))
self.figlet_container = cast(Container, self.query_one("#figlet_container"))
self.figlet_widget = cast(FigletWidget, self.query_one("#figlet_widget"))
self.font_select = cast(Select, self.query_one("#font_select"))
self.text_input = cast(TextArea, self.query_one("#text_input")) # chad type hinting convenience vars
self.font_switch = cast(Switch, self.query_one("#switch"))
Expand All @@ -117,20 +115,6 @@ def on_mount(self):
end = self.text_input.get_cursor_line_end_location()
self.text_input.move_cursor(end)


# NOTE: about the resize event:
# The widget is not capable of automatically responding to screen resize events.
# You have to call the update method manually when the screen is resized if you want it
# to auto wrap on screen resize.
# But when updating, the widget will automatically adjust to the size of its parent container.
# ie. if the parent container is set to 100% width, the widget will also adjust to 100% width.
def on_resize(self, event: Resize):
self.log(f"Resize event: {event.size}")
self.figlet_widget.update(resized=True)

# Notice that we don't need to set the size of the figlet_widget.
# The figlet_widget will automatically adjust to the size of its parent container.

@on(Select.Changed)
def font_changed(self, event: Select.Changed) -> None:
if event.value == Select.BLANK:
Expand Down Expand Up @@ -165,15 +149,15 @@ def set_container_size(self):
height = self.height_input.value
self.log(f"Setting container size to: ({width} x {height})")
if width:
self.figlet_container.styles.width = int(width)
self.figlet_widget.styles.width = int(width)
if height:
self.figlet_container.styles.height = int(height)
self.figlet_widget.styles.height = int(height)
if not width:
self.figlet_container.set_styles('width: 1fr;')
self.figlet_widget.set_styles('width: 1fr;')
if not height:
self.figlet_container.set_styles('height: auto;')
self.figlet_widget.set_styles('height: auto;')

self.figlet_widget.update(resized=True)
self.figlet_widget.update()

@on(FigletWidget.Updated)
def figlet_updated(self, event: FigletWidget.Updated):
Expand Down
156 changes: 99 additions & 57 deletions textual_pyfiglet/figletwidget.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,86 @@
from __future__ import annotations
from typing import cast
import os

from textual.message import Message
from textual.widgets import Static
from textual.containers import Container

from .pyfiglet import Figlet, fonts


class _InnerFiglet(Static):
"""This is a placeholder widget that will contain the PyFiglet text.
It's used to calculate the size of the PyFiglet text, and then the FigletWidget
will adjust its size to fit the text."""

DEFAULT_CSS = """
FigletWidget {
margin: 0;
padding: 0;
}
"""

def __init__(self, *args, font, justify, **kwargs) -> None:
"""Private class for the FigletWidget.
Args:
renderable: A Rich renderable, or string containing console markup.
font (PyFiglet): Font to use for the ASCII art. Default is "calvin_s".
expand: Expand content if required to fill container.
shrink: Shrink content if required to fill container.
markup: True if markup should be parsed and rendered.
name: Name of widget.
id: ID of Widget.
classes: Space separated list of class names.
disabled: Whether the static is disabled or not."""

super().__init__(*args, **kwargs)
self.stored_text = None # we init with no stored text
self.font = font
self.justify = justify
self.figlet = Figlet(font=font, justify=justify)

def update(self, new_text: str | None = None) -> None:
"""Custom update method for the FigletWidget.
This method is private so docstring is in the FigletWidget class."""
if new_text is not None:
self.stored_text = new_text

# for dev debugging
# self.log.debug(
# f'parent.size.width: {self.parent.size.width} | parent.size.height: {self.parent.size.height} \n'
# f' self.size.width: {self.size.width} | self.size.height: {self.size.height}'
# )

if self.parent.size.width == 0:
self.log.error('parent.size.width is 0. Exiting update.')
return
self.figlet.width = self.parent.size.width

self.renderable = self.figlet.renderText(self.stored_text)

# this line is very key to the widget resizing properly
# activates textual's layout system in some magical way.
self.refresh(layout=True)

# More dev debugging
# self.log.debug(f'update EXIT: parent.size: {self.parent.size} \n self.size: {self.size}')


class FigletWidget(Static):
"""Adds simple PyFiglet ability to the Static widget.
The easiest way to use this widget is to place it inside of a container,
to act as its parent container.
See __init__ for more details."""


DEFAULT_CSS = """
FigletWidget {
width: auto;
height: auto;
padding: 0;
}
"""

Expand All @@ -35,8 +97,6 @@ class FigletWidget(Static):
'tmplr'
]

update_timer = None

class Updated(Message):
"""This is here to provide a message to the app that the widget has been updated.
You might need this to trigger something else in your app resizing, adjusting, etc.
Expand All @@ -51,7 +111,7 @@ def control(self) -> FigletWidget:
return self.widget


def __init__(self, *args, font: str = "calvin_s", **kwargs) -> None:
def __init__(self, *args, font: str = "calvin_s", justify: str = "center", **kwargs) -> None:
"""A custom widget for turning text into ASCII art using PyFiglet.
This args section is copied from the Static widget. It's the same except for the font argument.
Expand All @@ -66,6 +126,7 @@ def __init__(self, *args, font: str = "calvin_s", **kwargs) -> None:
Args:
renderable: A Rich renderable, or string containing console markup.
font (PyFiglet): Font to use for the ASCII art. Default is "calvin_s".
justify (PyFiglet): Justification for the text. Default is "center".
expand: Expand content if required to fill container.
shrink: Shrink content if required to fill container.
markup: True if markup should be parsed and rendered.
Expand Down Expand Up @@ -94,81 +155,62 @@ def __init__(self, *args, font: str = "calvin_s", **kwargs) -> None:
super().__init__(*args, **kwargs)
self.stored_text = str(self.renderable)
self.font = font
self.figlet = Figlet(font=font)
self.justify = justify

# NOTE: Figlet also has a "direction" argument
# TODO Add Direction arguments

# NOTE: Figlet also has "direction" and "justify" arguments,
# but I'm not using them here yet. Should probably be added in the future.
# TODO Add Direction and Justify arguments
def compose(self):
yield _InnerFiglet(self.stored_text, id='inner_figlet', font=self.font, justify=self.justify)

def on_mount(self):
self.update()
self._inner_figlet = cast(_InnerFiglet, self.query_one('#inner_figlet'))
self.update(new_text=self.stored_text)

def update(self, new_text: str | None = None, resized: bool = False) -> None:
"""Update the PyFiglet area with the new text.
Note that this over-rides the standard update method in the Static widget.
This does NOT take any rich renderable like the Static widget does.
It can only take a text string.
def on_resize(self):
self._inner_figlet.update()

Note that if this update is for a resize event, such as window resize or container resize,
you MUST set resized to True. This will enable a slight delay which allows
the widget to adjust to the new size before rendering. This is important for the
update to work properly.
def update(self, new_text: str|None = None) -> None:
'''Update the PyFiglet area with the new text.
Note that this over-rides the standard update method in the Static widget.
Unlike the Static widget, this method does not take a Rich renderable.
It can only take a text string. Figlet needs a normal string to work properly.
Args:
new_text: The text to update the PyFiglet widget with. Default is None.
resized: If the widget has been resized. Default is False.
"""
new_text: The text to update the PyFiglet widget with. Default is None.'''

if new_text is not None:
self.stored_text = new_text
if resized:
if self.update_timer is not None:
self.update_timer.cancel()
self.set_timer(0.1, self._update_internal) # TODO Using a timer here is a bit of a hack.
else:
self._update_internal()

# NOTE: Ideally I'd like to find a way that's better than the timer.

def _update_internal(self, new_text: str | None = None) -> None:

# for dev debugging
self.log.debug(
f'parent.size.width: {self.parent.size.width} | parent.size.height: {self.parent.size.height} \n'
f' self.size.width: {self.size.width} | self.size.height: {self.size.height}'
)

if self.parent.size.width == 0:
self.log.debug('parent.size.width is 0. Exiting update.')
return
self.figlet.width = self.parent.size.width

self.renderable = self.figlet.renderText(self.stored_text)

# this makes textual reset the widget size to whatever the new renderable is
self.refresh(layout=True)

# Post a message to the app that the widget has been updated
self.post_message(self.Updated(self))

# More dev debugging
self.log.debug(f'update EXIT: parent.size: {self.parent.size} \n self.size: {self.size}')

self._inner_figlet.update(new_text=self.stored_text)

def set_font(self, font: str) -> None:
"""Set the font for the PyFiglet widget.
"""Set the font for the PyFiglet widget.
The widget will update with the new font automatically.
Pass in the name of the font as a normal string:
Pass in the name of the font as a string:
ie 'calvin_s', 'small', etc.
Args:
font: The name of the font to set."""

self._inner_figlet.figlet.setFont(font=font)
self.update()

self.figlet.setFont(font=font)
def set_justify(self, justify: str) -> None:
"""Set the justification for the PyFiglet widget.
The widget will update with the new justification automatically.
Pass in the justification as a string:
options are: 'left', 'center', 'right', 'auto'
Args:
justify: The justification to set."""

self._inner_figlet.figlet.setJustify(justify=justify)
self.update()

def get_fonts_list(self, get_all: bool = True) -> list:
"""Scans the fonts folder.
"""Scans the fonts folder.
Returns a list of all font filenames (without extensions).
Args:
Expand Down
Loading

0 comments on commit e5d31e1

Please sign in to comment.