LCD Manager – part 8: widget canvas

Thinking about Piader game view brought me to conclusion that we need a canvas widget. The widget on which we can draw any char at any place. Something similar to pure lcd usage. We will be able to set cursor at any position in canvas and put char. Internal pointer will allow us to stream chars one after another.

Overview

We will create a canvas widget. Such canvas needs to have height and width set at start, this mean we wont need autosize, and we need protection against it. For holding content we will use a simple array. Another variable to hold current cursor position. This is for start.
What else?
Function that can move cursor, another function that can write at position. Important thing, we wont break lines, so if string goes out of bound we will truncate it.
Clearing whole canvas could be nice too.

Lets get to work!

First test for proper initialization:

#!/usr/bin/python
# -*- coding: utf-8 -*-
#pylint: skip-file

"""Tests for canvas class"""

from nose.tools import assert_equal
from nose.tools import assert_raises
import lcdmanager.widget.canvas as canvas


class TestCanvas(object):
    def setUp(self):
        self.canvas = canvas.Canvas(1, 1, 5, 2, 'New', True)

    def test_should_initialize_correctly(self):
        assert_equal(self.canvas.position, {'x': 1, 'y': 1})
        assert_equal(self.canvas.x, 0)
        assert_equal(self.canvas.y, 0)
        assert_equal(self.canvas.visibility, True)
        assert_equal(self.canvas.autowidth, False)
        assert_equal(self.canvas.width, 5)
        assert_equal(self.canvas.height, 2)

Few words about it, we have x and y properties, they define cursor position on canvas.
Now lest create skeleton of canvas class:

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""Widget - canvas"""

import lcdmanager.abstract.widget as widget
import lcdmanager.manager as manager


class Canvas(widget.Widget):
    """Canvas widget"""
    def __init__(self, pos_x, pos_y, width, height, name=None, visibility=True):
        widget.Widget.__init__(self, pos_x, pos_y, name, visibility)
        self.width = width
        self.height = height
        self.screen = []
        self.clear()
        self.cursor_position = {
            'x': 0,
            'y': 0
        }

    def clear(self):
        """clear screen"""
        self.screen = [" ".ljust(self.width, " ")
                       for _ in range(0, self.height)]

    def render(self):
        """return view array"""
        return self.screen

    @property
    def x(self):
        """get x cursor position"""
        return self.cursor_position['x']

    @x.setter
    def x(self, value):
        """set x cursor position"""
        self.cursor_position['x'] = value

    @property
    def y(self):
        """get y cursor position"""
        return self.cursor_position['y']

    @y.setter
    def y(self, value):
        """set y cursor position"""
        self.cursor_position['y'] = value

Initialization require width and height parameter. We also have cursor x, y properties and render function. Not much but good for start.
Render function just returns screen variable, its responsible for holding content.
Before drawing lets add another short test for changing current cursor position

    def test_set_cursor_position(self):
        self.canvas.x = 3
        self.canvas.y = 1
        assert_equal(self.canvas.x, 3)
        assert_equal(self.canvas.y, 1)

Seems everything works.
Time for actual drawing. Function will add string to buffer.
Test that will add string and check output:

    def test_write_string(self):
        self.canvas.write("It's me")
        assert_equal(self.canvas.x, 7)
        self.buffer[0] = "It's me    "
        assert_equal(self.canvas.render(), self.buffer)

We are checking if cursor moves to new position and if render returns correct array.
Of course now it fails πŸ™‚
But when we write following function we are back to green:

    def write(self, string, pos_x=None, pos_y=None):
        """writes a string"""
        if pos_x is None:
            pos_x = self.cursor_position['x']
        if pos_y is None:
            pos_y = self.cursor_position['y']

        line = self.screen[pos_y]
        new_line = line[0:pos_x] + string + line[pos_x + len(string):]
        line = new_line[:self.width]
        self.screen[pos_y] = line

        self._inc_c(len(string))
        pass

    def _inc_c(self, length):
        """move a cursor by length"""
        self.x += length
        if self.x > self.width:
            self.x = self.width

Ok we can write a simple string. But what about this:

    def test_write_string_at_position(self):
        self.canvas.write("It's me", 3, 1)
        assert_equal(self.canvas.x, 7)
        self.buffer[1] = "   It's me "
        assert_equal(self.canvas.render(), self.buffer)

We are writing at specific position – we are still green. And this:

    def test_write_string_at_position(self):
        self.canvas.write("It's me", 3, 1)
        assert_equal(self.canvas.x, 7)
        self.buffer[1] = "   It's me "
        assert_equal(self.canvas.render(), self.buffer)

And we are still green. String was correctly truncated. But what will happens when we write string out of allowed positions? It shouldn’t crash:

    def test_write_string_at_position(self):
        self.canvas.write("It's me", 13, 11)
        assert_equal(self.canvas.x, 11)
        self.buffer[1] = "   It's me "
        assert_equal(self.canvas.render(), self.buffer)

It crashed 😦 We need to add check for bounds and for variable beyond it we will set its position at maximum:

    def write(self, string, pos_x=None, pos_y=None):
        """writes a string"""
        if pos_x is None:
            pos_x = self.cursor_position['x']
        if pos_y is None:
            pos_y = self.cursor_position['y']

        if pos_x < self.width and pos_y < self.height: line = self.screen[pos_y] new_line = line[0:pos_x] + string + line[pos_x + len(string):] line = new_line[:self.width] self.screen[pos_y] = line self._inc_c(len(string)) else: if pos_x > self.width:
                self.x = self.width
            if pos_y > self.height:
                self.y = self.height

Final test, write three strings:

    def test_write_three_strings(self):
        self.canvas.write("It's", 1)
        self.canvas.write(" me")
        self.canvas.write("For", 9, 0)
        self.canvas.write("sure", 2, 1)
        self.buffer[0] = " It's me Fo"
        self.buffer[1] = "  sure     "
        assert_equal(self.canvas.render(), self.buffer)

And we are in red. Quick debug and we have a bug with calculating position and whats more funny two tests are wrongly written! Why ?
Look at test_write_string_at_position. We are checking if x = 7, but we are writing from pos_x = 3! It should be 10 πŸ™‚ After fixing tests and write function we are back to green. New write function:

    def write(self, string, pos_x=None, pos_y=None):
        """writes a string"""
        if pos_x is None:
            pos_x = self.cursor_position['x']
        else:
            self.cursor_position['x'] = pos_x
        if pos_y is None:
            pos_y = self.cursor_position['y']

        if pos_x < self.width and pos_y < self.height: line = self.screen[pos_y] new_line = line[0:pos_x] + string + line[pos_x + len(string):] line = new_line[:self.width] self.screen[pos_y] = line self._inc_c(len(string)) else: if pos_x > self.width:
                self.x = self.width
            if pos_y > self.height:
                self.y = self.height

What we need more? Ah yes clear function. Lets write a test:

    def test_clear(self):
        self.canvas.write("It's", 1)
        self.canvas.write(" me")
        self.canvas.clear()
        assert_equal(self.canvas.render(), self.buffer)

I predicted this and we are still green πŸ™‚

Demo

We have some code a some test. In theory it will work. But our small demo will verify this.


#!/usr/bin/python
# -*- coding: utf-8 -*-

"""LCD Manager - canvas demo"""

import sys
sys.path.append("../../")
import RPi.GPIO as GPIO #pylint: disable=I0011,F0401
from charlcd import buffered as buffered
import charlcd.drivers.gpio as gp
from lcdmanager import manager
from lcdmanager.widget.canvas import Canvas

GPIO.setmode(GPIO.BCM)


def demo1():
    """20x4 demo"""
    lcd = buffered.CharLCD(20, 4, gp.Gpio())
    lcd.init()
    lcd_manager = manager.Manager(lcd)
    canvas1 = Canvas(0, 0, 20, 4)
    canvas1.write('Hi !', 4, 1)
    lcd_manager.add_widget(canvas1)
    lcd_manager.render()
    lcd_manager.flush()

demo1()

For me string displays on lcd:)

Stop! Jenkins time!

PyLint shown what I was afraid about, that use an x and y variables is wrong. But this is a moment when we will ignore it. For our code is much cleaner to use x and y.

Summary

We created another widget. This time it was canvas one. It allows us to write string at any position within it. This one we will use as game board in our Piader game.

Download code

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s