Skip to content

bitmap_label: Make text, line_spacing and scale mutable #90

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Aug 31, 2020

Conversation

kmatch98
Copy link
Contributor

@kmatch98 kmatch98 commented Aug 27, 2020

bitmap_label updates

This update makes text, line_spacing and scale mutable.

Other changes:

  • Add option to use bitmap.blit on CircuitPython builds where the function is available, but will default back to Python blit when not available

Other details on the changes are in a separate post below.

@kmatch98
Copy link
Contributor Author

kmatch98 commented Aug 27, 2020

The new display_text library bitmap_label utilizes a bitmap to display the text. For this reason, if the text or line_spacing is changed, then the whole bitmap has to be regenerated. It took me a while to figure out how to make the bitmap mutable, so here is my saga of how I figured out how to make it work. Also, the mutability of scale was also made more complex due to some limitations in CircuitPython or MicroPython.

Mutability of the self.bitmap

A bitmap_label is defined as subclass of Group. This Group is similar to a list that can contain displayio items (max_items). The items in a Group are “mutable” meaning that we can change what they are by popping them out of the group and adding them into the Group using insert or append.

In our case of an instance of bitmap_label, we use super().init(max_size=1) to define the bitmap_label as a Group with one item. Then, we create a displayio.TileGrid and append that into our self Group. This TileGrid contains our bitmap of text.

So, to reiterate, the self Group contents can be changed. So, if we want to change the text bitmap, we can create a new bitmap, put that into a new TileGrid, and then place that into the self Group.

From the outside world, we could create a bitmap_label as follows:

import board
import terminalio
from adafruit_display_text import bitmap_label as label

my_label=label.Label(terminalio.FONT, text=“Hello world!”)
board.DISPLAY.show(my_label)
while True:
	pass

This creates a bitmap_label (a Group) called my_label that shows the text “Hello world!”. The bitmap_label library creates a Bitmap (my_label.bitmap = self.bitmap) with the text rendered and puts that the Bitmap into a new TileGrid (my_label.tilegrid = self.tilegrid). Then, the bitmap_label library appends the TileGrid onto the my_label Group.

When you perform board.DISPLAY.show(my_label), this puts the my_label Group onto the display.

Now, see how to change the text.

My first attempt did not work!

I originally thought that I needed to recreate the bitmap_label Group by calling super().init again. However that is exactly the wrong thing! Here’s why. Once the my_label is shown on the display, the display is told to reference whatever my_label represents in the microcontroller. If we call super().init again, then we are in instancing a new Group object. However, even though we call this new Group by the same name (my_label), the display is still referencing to the original reference for my_label. By calling board.DISPLAY.show(my_label), the display doesn’t really know the name “my_label”. All it really knows is “someone told me to reference this specific reference”. It happened to be passed as the name “my_label”, but all display knows is the reference that “my_label” was originally referencing when board.DISPLAY.show(my_label) was called.

So, here’s why this isn’t the right way of changing the bitmap. If you instance a new Group with super().init (called the same thingmy_label), the display doesn’t get informed that my_label’s reference has changed. The display still is referencing whatever it originally referenced when you called board.DISPLAY.show().

Things that do work!

After the trial and error above, I realized that the display won’t know the Group reference changes, so we have to keep the Group the same! However, I remembered that a Group’s contents can be changed by pop(), insert() and append(). So, we don’t want to create a new Group, we just want to modify the Group’s “contents” with pop, insert and append!

Summary of self.bitmap

Ok, now let’s get to work on making bitmap_label mutable. Let’s recap the structure of bitmap_label.
A bitmap_label is a Group containing one item, a TileGrid containing a bitmap.

So, let’s keep the Group the same, but let’s pop() off the original TileGrid and put a new TileGrid with a new Bitmap.
That’s pretty much it.

scale made things more complicated

After completing the above, I realized that the scale also needed to be updated for mutability. In the previous version, the self group's scale parameter was modified to change the scale on the display. However, I realized that if the scale is changed, the anchored_position is not properly updated. So, somehow I needed to "intercept" any calls to change the scale and then also call an update to anchored_position.

I ran into some limitations in CircuitPython that will be mentioned here. A reminder of the structure of the Label class:

  • Label is a subclass of Group

So, I need to "override" the getter and setter of Group.scale and redefine it to also call an update of anchored_position. In regular Python, a child class can call a Parent's getter/setters even if they are overridden by the child class using notation of __set__ or __get__ or fset or fget. However, after discussion with @dhalbert, @sommersoft and @FoamyGuy, I could not find a way of access a Parent class' getter/setter. This seems to be a structural decision by MicroPython, but I could be wrong. Here is one issue that discusses the use of "property descriptors". Here is a second issue related to "property descriptors"..

Due to these limitations, I had to come up with a way of:

  1. Leaving the self Group's scale to be unchanged
  2. Find somehow else respond to changes in scale

To solve these issues, I chose to modify the structure as follows:

  • Label is a subclass of Group
  • Label's self Group contains one Group: self.local_group
  • self.local_group contains one tileGrid: self.tilegrid
  • self.tilegrid contains one bitmap: self.bitmap

To summarize, I added an intervening Group self.local_group that I chould change the scale using self.local_group.scale=value. This added another layer of complexity and is not elegant, but it works. While I didn't do any detailed performance comparison, brief testing didn't show any major degradation in response time of using this additional intervening Group.

@kmatch98
Copy link
Contributor Author

Here is the text code that I used to evaluate the mutability of bitmap_label. Please note that font and line_spacing are immutable when save_text=False.

import board
import terminalio
import displayio
import gc
from adafruit_display_text import bitmap_label as label
#from adafruit_display_text import label
gc.collect()
mem_start=gc.mem_free()

#  Setup the SPI display
if "DISPLAY" not in dir(board):
    # Setup the LCD display with driver
    # You may need to change this to match the display driver for the chipset
    # used on your display
    from adafruit_ili9341 import ILI9341

    displayio.release_displays()

    # setup the SPI bus
    spi = board.SPI()
    tft_cs = board.D9  # arbitrary, pin not used
    tft_dc = board.D10
    tft_backlight = board.D12
    tft_reset = board.D11

    while not spi.try_lock():
        spi.configure(baudrate=32000000)
    spi.unlock()

    display_bus = displayio.FourWire(
        spi,
        command=tft_dc,
        chip_select=tft_cs,
        reset=tft_reset,
        baudrate=32000000,
        polarity=1,
        phase=1,
    )

    # Number of pixels in the display
    DISPLAY_WIDTH = 320
    DISPLAY_HEIGHT = 240

    # create the display
    display = ILI9341(
        display_bus,
        width=DISPLAY_WIDTH,
        height=DISPLAY_HEIGHT,
        rotation=180,  # The rotation can be adjusted to match your configuration.
        auto_refresh=True,
        native_frames_per_second=90,
    )

    # reset the display to show nothing.
    display.show(None)
else:
    # built-in display
    display = board.DISPLAY

print("Display is started")

import time


fontList = []

# Load some proportional fonts


from adafruit_bitmap_font import bitmap_font

fontFile = "fonts/Helvetica-Bold-16.bdf"
fontToUse = bitmap_font.load_font(fontFile)
glyphs=b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_ = 1234567890 M j\'"

print('loading glyphs')
fontToUse.load_glyphs(glyphs)

print('starting timer')
start=time.monotonic()


#Create initial text with terminalio.FONT
text = "Hello world"
text_area = label.Label(terminalio.FONT, x=10, y=10, text=text, max_glyphs=73, scale=1)


#text='Trial with:\nsave_text=False'
## Verify whether save_text=False triggers error when changing line_spacing and font
#text_area = label.Label(terminalio.FONT, text=text, max_glyphs=73, scale=1, save_text=False)


display.show(text_area)
time.sleep(0.1)
time.sleep(1.0)


# move x,y positions
text_area.x = 30
text_area.y = 70
time.sleep(0.1)
time.sleep(1.0)


# Change the text
text_area.text="change it up"
time.sleep(0.1)
time.sleep(2)


# add a newline
text_area.text="change it up\nwith two lines"
time.sleep(0.1)
time.sleep(2)

# Change font
display.auto_refresh=False
text_area.text="switching to Helvetica"
text_area.font=fontToUse
display.auto_refresh=True
time.sleep(0.1)
time.sleep(2)


# Make a long text line
text_area.text="change it up\nwith lines\nand lines\nand lines"
time.sleep(0.1)
time.sleep(2)

# Change to yellow background
text_area.background_color=0xFFFF00
time.sleep(0.1)
time.sleep(2)

# Change to black text
text_area.color=0x000000
time.sleep(0.1)
time.sleep(2)


# Change to line_spacing=3

display.auto_refresh=False
text_area.line_spacing=3
text_area.text="change it up\nwith a long line\nspacing=3"
display.auto_refresh=True
time.sleep(0.1)
time.sleep(2)

# Set linespacing back to 1, change background color to green, center on screen 
display.auto_refresh=False
text_area.text="change it up\nwith a long line\nline_spacing=1\ncentered"
text_area.line_spacing=1
text_area.background_color=0x00FF00
text_area.anchor_point=(0.5,0.5)
text_area.anchored_position=(320/2, 240/2)
display.auto_refresh=True
time.sleep(0.1)
time.sleep(2)


# check scaling with Helvetica
display.auto_refresh=False
text_area.text="change it up\nwith a long line\nscale=2\ncentered"
text_area.scale=2
display.auto_refresh=True
time.sleep(0.1)
time.sleep(2)

# Verify that scale is persistent with a text change
display.auto_refresh=False
text_area.text='terminalio.FONT\nscale=2\ncentered'
text_area.font=terminalio.FONT
display.auto_refresh=True
time.sleep(0.1)
time.sleep(2)

display.auto_refresh=False
text_area.text='terminalio.FONT\nscale=1\ncentered'
text_area.scale=1
display.auto_refresh=True
time.sleep(0.1)
time.sleep(2)


end=time.monotonic()

gc.collect()
mem_end=gc.mem_free()
print('duration: {} seconds'.format(end-start))
print('mem_used {}'.format(mem_start-mem_end))


while True:
    pass

@kmatch98
Copy link
Contributor Author

Here is my latest test code for these two updates:


import board
import terminalio
import displayio
import gc
from adafruit_display_text import bitmap_label as label
#from adafruit_display_text import label
gc.collect()
mem_start=gc.mem_free()

#  Setup the SPI display
if "DISPLAY" not in dir(board):
    # Setup the LCD display with driver
    # You may need to change this to match the display driver for the chipset
    # used on your display
    from adafruit_ili9341 import ILI9341

    displayio.release_displays()

    # setup the SPI bus
    spi = board.SPI()
    tft_cs = board.D9  # arbitrary, pin not used
    tft_dc = board.D10
    tft_backlight = board.D12
    tft_reset = board.D11

    while not spi.try_lock():
        spi.configure(baudrate=32000000)
    spi.unlock()

    display_bus = displayio.FourWire(
        spi,
        command=tft_dc,
        chip_select=tft_cs,
        reset=tft_reset,
        baudrate=32000000,
        polarity=1,
        phase=1,
    )

    # Number of pixels in the display
    DISPLAY_WIDTH = 320
    DISPLAY_HEIGHT = 240

    # create the display
    display = ILI9341(
        display_bus,
        width=DISPLAY_WIDTH,
        height=DISPLAY_HEIGHT,
        rotation=180,  # The rotation can be adjusted to match your configuration.
        auto_refresh=True,
        native_frames_per_second=90,
    )

    # reset the display to show nothing.
    display.show(None)
else:
    # built-in display
    display = board.DISPLAY

print("Display is started")

import time


fontList = []

# Load some proportional fonts


from adafruit_bitmap_font import bitmap_font

fontFile = "fonts/Helvetica-Bold-16.bdf"
fontToUse = bitmap_font.load_font(fontFile)
glyphs=b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_ = 1234567890 M j\'"

print('loading glyphs')
fontToUse.load_glyphs(glyphs)

print('starting timer')
start=time.monotonic()


#Create initial text with terminalio.FONT
text = "Hello world"
text_area = label.Label(terminalio.FONT, text=text, max_glyphs=73)

display.show(text_area)
time.sleep(0.1)
time.sleep(1.0)

# move x,y positions
text_area.x = 30
text_area.y = 70
time.sleep(0.1)
time.sleep(1.0)

# Change the text
text_area.text="change it up"
time.sleep(0.1)
time.sleep(2)

# add a newline
text_area.text="change it up\nwith two lines"
time.sleep(0.1)
time.sleep(2)

# add a newline
text_area.text="change it up\nwith\nline_spacing=3"
text_area.line_spacing=3
time.sleep(0.1)
time.sleep(2)

# add a newline
text_area.text="change it up\nwith\nline_spacing=1"
text_area.line_spacing=1
time.sleep(0.1)
time.sleep(2)

# Change font
display.auto_refresh=False
text_area.text="switching to Helvetica"
text_area.font=fontToUse
display.auto_refresh=True
time.sleep(0.1)
time.sleep(2)

# Make a long text line
text_area.text="change it up\nwith lines\nand lines\nand lines"
time.sleep(0.1)
time.sleep(2)

# Change to yellow background
text_area.background_color=0xFFFF00
time.sleep(0.1)
time.sleep(2)

# Change to black text
text_area.color=0x000000
time.sleep(0.1)
time.sleep(2)

# Change to line_spacing=3
display.auto_refresh=False
text_area.line_spacing=3
text_area.text="change it up\nwith a long line\nspacing=3"
display.auto_refresh=True
time.sleep(0.1)
time.sleep(2)

#Set linespacing back to 1, center on screen 
display.auto_refresh=False
text_area.line_spacing=1
text_area.text="change it up\nwith a long line\nline_spacing=1\ncentered"
text_area.anchor_point=(0.5,0.5)
text_area.anchored_position=(320/2, 240/2)
display.auto_refresh=True
time.sleep(0.1)
time.sleep(2)

display.auto_refresh=False
text_area.text="change it up\nwith a long line\nline_spacing=1\nBLUE ***"
text_area.line_spacing=1
text_area.background_color=0x0000FF
text_area.color=0xFFFFFF
display.auto_refresh=True
time.sleep(0.1)
time.sleep(2)

# check scaling with Helvetica
display.auto_refresh=False
text_area.line_spacing=1
text_area.text="change it up\nwith\nscale=2"
text_area.scale=2
display.auto_refresh=True
time.sleep(0.1)
time.sleep(2)

# Verify that scale is persistent with a font change
display.auto_refresh=False
text_area.text='terminalio.FONT\nscale=2\ncentered'
text_area.font=terminalio.FONT
display.auto_refresh=True
time.sleep(0.1)
time.sleep(2)

# Scale down to 1
display.auto_refresh=False
text_area.text='terminalio.FONT\nscale=1\ncentered'
text_area.scale=1
display.auto_refresh=True
time.sleep(0.1)
time.sleep(2)


end=time.monotonic()

gc.collect()
mem_end=gc.mem_free()
print('duration: {} seconds'.format(end-start))
print('mem_used {}'.format(mem_start-mem_end))


while True:
    pass

Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few questions but nothing major. The local group is fine as a way to control scale.

…it, add back kwargs passing to self Group instance
@kmatch98
Copy link
Contributor Author

@tannewt Thanks for the detailed code review and the helpful suggestions. I think I responded correctly to all your comments with a new commit.

Also, please note that I updated the docs/api.rst file to include bitmap_label into the readthedocs documentation.

Copy link
Contributor

@FoamyGuy FoamyGuy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested the latest version of changes out with a Minitft Featherwing and Feather Sense, using several bitmap labels that change text and scale. No issues to report.

Code looks good to me as well. Thanks for working on these enhancements @kmatch98.

Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! Thank you!

@tannewt tannewt merged commit a836740 into adafruit:master Aug 31, 2020
adafruit-adabot added a commit to adafruit/Adafruit_CircuitPython_Bundle that referenced this pull request Sep 4, 2020
Updating https://github.com/adafruit/Adafruit_CircuitPython_MCP230xx to 2.4.1 from 2.4.0:
  > Merge pull request adafruit/Adafruit_CircuitPython_MCP230xx#33 from jepler/remove-bad-whitespace-directive

Updating https://github.com/adafruit/Adafruit_CircuitPython_MS8607 to 1.0.2 from 1.0.1:
  > Merge pull request adafruit/Adafruit_CircuitPython_MS8607#2 from adafruit/dependency

Updating https://github.com/adafruit/Adafruit_CircuitPython_PyPortal to 3.3.1 from 3.3.0:
  > Merge pull request adafruit/Adafruit_CircuitPython_PyPortal#84 from makermelissa/master

Updating https://github.com/adafruit/Adafruit_CircuitPython_Bitmap_Font to 1.2.2 from 1.2.0:
  > Merge pull request adafruit/Adafruit_CircuitPython_Bitmap_Font#29 from adafruit/tannewt-patch-1
  > Merge pull request adafruit/Adafruit_CircuitPython_Bitmap_Font#28 from ronfischler/set-changed-during-iteration-fix

Updating https://github.com/adafruit/Adafruit_CircuitPython_Display_Text to 2.9.0 from 2.8.3:
  > Merge pull request adafruit/Adafruit_CircuitPython_Display_Text#90 from kmatch98/bitmap_mutable

Updating https://github.com/adafruit/Adafruit_CircuitPython_ImageLoad to 0.11.7 from 0.11.6:
  > Merge pull request adafruit/Adafruit_CircuitPython_ImageLoad#38 from tannewt/run_tests

Updating https://github.com/adafruit/Adafruit_CircuitPython_Motor to 3.2.3 from 3.2.2:
  > Merge pull request adafruit/Adafruit_CircuitPython_Motor#47 from tannewt/run_tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants