AASDecoder for HDRadio PNG Images

This was written as a decoder for the Teensy HDRadio project using a Si468X from SiLabs

#ifndef LOTDEC_h
#define LOTDEC_h

#include "SDCard.h"
#include "yspng.h"
#include "tjpgd.h"

#define FILE_MAX 25 * 1024 // KB blocks memory per file/port
#define MAX_PORTS 2        // max data ports per audio program

#define FILEPATH "aasfiles"

#define JPEGMEM 4000 // memory buffer for TJpg
#define COLOR_BCKGND 0x0
#define OUTPIXL 100

#define AAS_PATH 20
#define AAS_ART "aas/%s%04x.ooo"
#define AAS_LOGO "aas/%s%d.ooo"

#define TEMP_PORT "aas/%04x.tmp"

extern SDCard sdcard;

enum IMAGEFORMAT
{
    NON = 0,
    JPG = 1,
    PNG = 2,
    RGB = 3
};

void newImageReceived(uint16_t lotid);

struct FileInfoHolder
{
    uint16_t length, size, sequence;
    File file;
    bool startcollection = false;
    char filename[AAS_PATH];
    char tempfilename[AAS_PATH];

    void reset(bool clearname)
    {
        length = 0;
        sequence = 0;
        size = 0;
        startcollection = 0;
        if (clearname)
        {
            memset(filename, 0, AAS_PATH);
            memset(tempfilename, 0, AAS_PATH);
        }
    }
};

struct PORTDataHolder
{
    uint16_t portid = 0;
    FileInfoHolder finfo; // assume one port produces one file stream

    void store(const char *sisshortname, const uint8_t hdprogram, const uint16_t port, uint8_t *inbff, const uint16_t length)
    {
        if (port != portid)
            return;

        const uint16_t LOTId = (inbff[3] << 8) | inbff[2];
        uint8_t *buf = inbff;
        uint32_t *p32 = (uint32_t *)buf;
        const uint32_t insequence = p32[1];
        uint16_t len = length;

        buf += 8;
        len -= 8;

        if (insequence == 0)
        {
            p32 = (uint32_t *)buf;
            uint32_t infilesize = p32[2];

            if (infilesize > FILE_MAX)
            {
#ifdef DEBUG
                Serial.printf("LOTDecoder P[%02x] : File too large... will not store\n", portid);
#endif
                return;
            }

            buf += 16;
            len -= 16;

            uint8_t *p = (uint8_t *)memchr(buf, '.', len - 16);

            if (p == NULL)
            {
#ifdef DEBUG
                Serial.printf("LOTDecoder P[%02x] : File has invalid name\n", portid);
#endif
                finfo.reset(true);
                return;
            }

            uint8_t namelen = p - buf + 4;
            char inboundfilename[namelen + 1];
            memset(inboundfilename, 0, namelen + 1);
            memcpy(inboundfilename, buf, namelen);
            finfo.reset(true);

#ifdef DEBUG
            Serial.printf("LOTDecoder P[%02x] : File >> %s, %d bytes\n", portid, inboundfilename, infilesize);
#endif

            bool proceed = determineFileName(sisshortname, hdprogram, inboundfilename, LOTId);

            if (!proceed)
                return;

            buf += namelen;
            len -= namelen;

            // check if exists?
            bool exists = checkifexists(infilesize);
            if (exists)
            {
#ifdef DEBUG
                Serial.printf("LOTDecoder P[%02x] : file exists: %s\n", portid, finfo.filename);
#endif
                // dont expect any bytes
            }
            else
            {
                newOutFile();
                if (finfo.file)
                {
#ifdef DEBUG
                    Serial.printf("LOTDecoder P[%02x] : File %s, size %d, START\n", port, finfo.tempfilename, infilesize);
#endif
                    writeOutFile(buf, len);
                    finfo.sequence = 1;
                    finfo.size = infilesize;
                    finfo.startcollection = true;
                }
            }
        }
        else if (insequence == finfo.sequence)
        {
            if (finfo.startcollection)
            {
                finfo.sequence++;
                if (finfo.length + len > finfo.size)
                {
#ifdef DEBUG
                    Serial.printf("LOTDecoder P[%02x] : File %s overflowed\n", port, finfo.tempfilename);
#endif
                    discardOutFile();
                    return;
                }

                writeOutFile(buf, len);
#ifdef DEBUG
                Serial.printf("LOTDecoder P[%02x] : File %s, %d (%.1f %%)\n", portid, finfo.tempfilename, finfo.length, (float)finfo.length * 100 / finfo.size);
#endif
                if (finfo.length == finfo.size)
                {
#ifdef DEBUG
                    Serial.printf("LOTDecoder P[%02x] : File %s, size %d, SAVED\n", portid, finfo.tempfilename, finfo.size);
#endif
                    closeOutFile();
                    if (convertOutFile())
                        newImageReceived(LOTId);
                }
            }
        }
        else if (finfo.size)
        {
#ifdef DEBUG
            Serial.printf("LOTDecoder P[%02x] : %s expected %d, got %d\n", portid, finfo.tempfilename, finfo.sequence, insequence);
#endif
        }
    }

    void writeOutFile(uint8_t *buf, uint16_t len)
    {
        if (finfo.file)
        {
            finfo.file.write(buf, len);
            finfo.length += len;
        }
    }

    void discardOutFile()
    {
        if (finfo.file)
        {
            finfo.file.close();
            if (sdcard.remove(finfo.tempfilename)) // delete lot file
            {
#ifdef DEBUG
                Serial.printf("LOTDecoder P[%02x] : File %s, DELETED\n", portid, finfo.tempfilename);
#endif
            }
            if (sdcard.remove(finfo.filename)) // delete ooo file
            {
#ifdef DEBUG
                Serial.printf("LOTDecoder P[%02x] : File %s, DELETED\n", portid, finfo.filename);
#endif
                finfo.reset(false);
            }
        }
    }

    void newOutFile()
    {
        sprintf(finfo.tempfilename, TEMP_PORT, portid);
        discardOutFile();
        finfo.file = sdcard.open(finfo.tempfilename, FILE_WRITE);
    }

    void closeOutFile()
    {
        if (finfo.file)
        {
            finfo.file.close();
            finfo.reset(false);
        }
    }

    bool convertOutFile()
    {
        bool status = false;

        if (sdcard.exists(finfo.tempfilename))
        {
#ifdef DEBUG
            long tm = millis();
#endif

            IMAGEFORMAT imgfmt;
            uint8_t buffer[2];
            uint16_t tsize = 0;

            File f = sdcard.open(finfo.tempfilename);
            if (f)
            {
                tsize = f.fileSize();
                f.read(buffer, 2);
                f.close();
            }

            const byte jpgh[2] = {0xFF, 0xD8};
            const byte pngh[2] = {0x89, 0x50};

            if (memcmp(buffer, jpgh, 2) == 0)
            {
                imgfmt = JPG;
            }
            else if (memcmp(buffer, pngh, 2) == 0)
            {
                imgfmt = PNG;
            }
            else
            {
                return false;
            }

            //

            uint16_t tot = OUTPIXL * OUTPIXL;

            uint16_t outb[tot];
            for (int s = 0; s < tot; s++)
            {
                outb[s] = COLOR_BCKGND;
            }

            if (imgfmt == JPG)
            {
                TJpg tjpg(0, 0, outb);
                if (tjpg.Decode(finfo.tempfilename) == JDR_OK)
                    status = true;

                // tjpg.openJPG(finfo.tempfilename, 0, 0, outb);
                // JDEC jdec;

                // uint8_t work[JPEGMEM];
                // if (tjpg.jd_prepare(&jdec, work, JPEGMEM) == JDR_OK)
                // {
                //     Serial.printf("Image: %u by %u. %u bytes used. ", jdec.width, jdec.height, JPEGMEM - jdec.sz_pool);
                //     if (tjpg.jd_decomp(&jdec, 1) == JDR_OK)
                //         status = true;
                // }
                // tjpg.closeJPG();
            }
            else if (imgfmt == PNG)
            {
                YsRawPngDecoder png(0, 0, COLOR_BCKGND, outb);
                if (png.Decode(finfo.tempfilename) == YSOK)
                    status = true;
            }

            if (status)
            {
                if (sdcard.exists(finfo.filename))
                    sdcard.remove(finfo.filename);

                File fout = sdcard.open(finfo.filename, FILE_WRITE);
                if (fout)
                {
                    uint8_t header[8] = {'R', 'G', 'B', OUTPIXL, OUTPIXL, (uint8_t)(tsize >> 8), (uint8_t)(tsize & 0xFF), 0};
                    fout.write(header, 8);
                    uint8_t *pntr = (uint8_t *)outb;
                    fout.write(pntr, tot * 2);
                    fout.close();
#ifdef DEBUG
                    tm = millis() - tm;
                    Serial.printf("LOTDecoder P[%02x] : File %s -> %s, CONVERTED %dms\n", portid, finfo.tempfilename, finfo.filename, tm);
#endif
                }
            }
        }

        sdcard.remove(finfo.tempfilename);

        return status;
    }

    bool checkifexists(uint16_t size)
    {
        uint32_t existsize = 0;

        if (sdcard.exists(finfo.filename))
        {
            File f = sdcard.open(finfo.filename);
            if (f)
            {
                uint8_t head[8];
                f.read(head, 8);
                f.close();
                if (memcmp(head, "RGB", 3) == 0)
                {
                    existsize = (head[5] << 8) | head[6];
#ifdef DEBUG
                    Serial.printf("LOTDecoder P[%02x] : File << %s, %d bytes\n", portid, finfo.filename, existsize);
#endif
                }
            }
        }

        if (existsize == 0)
            return false;
        else
        {
            if (existsize != size) // compare filesize
            {
                sdcard.remove(finfo.filename);
#ifdef DEBUG
                Serial.printf("LOTDecoder P[%02x] : File removed %s, Filesize not matching\n", portid, finfo.filename);
#endif
                return false;
            }
            else
                return true;
        }
    }

    bool determineFileName(const char *sisshortname, const uint8_t hdprogram, char *infilename, uint16_t lotid)
    {
        const uint8_t fl = strlen(infilename);

        if (fl < 6)
            return false;

        if (memcmp(infilename, "SL", 2) == 0)
        { // station logo
            // SLWISX$$01109820000.png -> WISX1.lot
            // SLWDAS$$01109910000.png -> WDAS1.lot
            // SLWBEN$$130005.jpg

            // uint8_t hdprogram = 1;
            // if (fl > 9)
            // {
            //     hdprogram = infilename[9] - '0';
            // }
            sprintf(finfo.filename, AAS_LOGO, sisshortname, hdprogram + 1);
            return true;
        }
        else
        { // album art
            // WISX016c60.jpg -> WISX6c60.lot
            // WRFF017b57.jpg -> WRFF3fd2.lot
            sprintf(finfo.filename, AAS_ART, sisshortname, lotid);
            return true;
        }
    }

    bool write_file(char *outname, uint8_t *buff, uint16_t size)
    {
        if (sdcard.available() && (sdcard.freeMB > 1))
        {
            uint32_t existsize = 0;

            if (sdcard.exists(outname))
            {
                File f = sdcard.open(outname);
                existsize = f.fileSize();
                f.close();
            }

            if (existsize != size) // compare filesize
            {
                if (existsize)
                {
                    if (sdcard.remove(outname)) // remove other size
                        existsize = 0;
                }

                if (existsize == 0)
                {
                    File file = sdcard.open(outname, FILE_WRITE);
                    if (file)
                    {
                        uint16_t remaining = size;
                        uint8_t *ptr = buff;

                        while (remaining > 512)
                        {
                            file.write(ptr, 512);
                            ptr += 512;
                            remaining -= 512;
                        }

                        file.write(ptr, remaining);
                        file.close();
                        return 1;
                    }
                }
            }
#ifdef DEBUG
            else
            {
                Serial.printf("LOTDecoder P[%02x] : file exists: %s\n", portid, outname);
            }
#endif
        }
        return 0;
    }

    void reset()
    {
#ifdef DEBUG
        Serial.printf("LOTDecoder P[%02x] : reset()\n", portid);
#endif
        discardOutFile();
        portid = 0;
        // delete all files received for the ports previously setup?
    }
};

class LOTDecoder
{
  public:
    void reset()
    {
        clearPortSetup();
    }

    bool setupPort(uint16_t port)
    {
        bool s = putPortSetup(port);
#ifdef DEBUG
        Serial.printf("LOTDecoder P[%02x] : setupPort() %d\n", port, s);
#endif
        return s;
    }

    void processPortData(char *shortname, const uint8_t hdprogram, uint16_t port, uint8_t *buffer, uint16_t length)
    {
        // #ifdef DEBUG
        //         Serial.printf("processPortData P[%02x] %d bytes\n", port, length);
        // #endif
        signed char p = isPortSetup(port);
        if ((p > -1) && (p < MAX_PORTS))
        {
            portdata[p].store(shortname, hdprogram, port, buffer, length);
        }
    }

    IMAGEFORMAT checkLotIMGFormat(char *filename)
    {
        uint8_t buffer[3];
        uint32_t size = 0;

        File f = sdcard.open(filename);
        if (f)
        {
            size = f.fileSize();
            f.read(buffer, 3);
            f.close();
        }

        // const byte jpgh[2] = {0xFF, 0xD8};
        // const byte pngh[2] = {0x89, 0x50};
        // const byte rgbh[3] = {'R', 'G', 'B'};

        // #ifdef DEBUG
        //         Serial.printf("FILE DATA {0x%02x, 0x%02x}\n", buffer[0], buffer[1]);
        // #endif

        // if (memcmp(buffer, jpgh, 2) == 0)
        // {
        //     return JPG;
        // }
        // else if (memcmp(buffer, pngh, 2) == 0)
        // {
        //     return PNG;
        // }
        // else
        if ((memcmp(buffer, "RGB", 3) == 0) && (size < FILE_MAX))
        {
            return RGB;
        }
        else
            return NON;
    }

    IMAGEFORMAT getXHDRFile(const uint16_t lotid, const uint8_t hdprogram, char *shortname, char *filename, uint8_t length)
    {
#ifdef DEBUG
        Serial.printf("getXHDRFile lotid %4x, [%s] HD %d\n", lotid, shortname, hdprogram + 1);
#endif
        IMAGEFORMAT imgfmt = NON;
        extern SDCard sdcard;
        if (sdcard.available())
        {
            if (lotid) // check album art
            {
                sprintf(filename, AAS_ART, shortname, lotid);
            }
            else
            {
                sprintf(filename, AAS_LOGO, shortname, hdprogram + 1);
            }

            if (sdcard.exists(filename))
            {
                imgfmt = checkLotIMGFormat(filename);
            }

            if (lotid && (imgfmt == NON)) // fallback to station logo
            {
                sprintf(filename, AAS_LOGO, shortname, hdprogram + 1);
                if (sdcard.exists(filename))
                {
                    return checkLotIMGFormat(filename);
                }
            }
        }
        return imgfmt;
    }

    PORTDataHolder portdata[MAX_PORTS];

    signed char isPortSetup(uint16_t port)
    {
        for (int x = 0; x < MAX_PORTS; x++)
        {
            if (port == portdata[x].portid)
                return x;
        }
        return -1;
    }

    void clearPortSetup()
    {
        for (int x = 0; x < MAX_PORTS; x++)
        {
            if (portdata[x].portid)
                portdata[x].reset();
        }
    }

    bool putPortSetup(uint16_t port)
    {
        for (int x = 0; x < MAX_PORTS; x++)
        {
            if (portdata[x].portid == 0)
            {
                portdata[x].portid = port;
                portdata[x].finfo.reset(true);
                return true;
            }
        }
        return false;
    }
};

#endif

Draggable Calculator Web Component

We are going to use the following imports for this Web Component:

import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { GestureEventListeners } from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';

Also, all CSS variables such as font-size: var(–lumo-font-size-s);
are defined at https://cdn.vaadin.com/vaadin-lumo-styles/1.5.0/demo/colors.html

Next, we define our CalculatorWidget class extending GestureEventListeners, which Polymer Library provides to support gestures (touch and mouse events).

class CalculatorWidget extends GestureEventListeners(PolymerElement) {
        static get is() {
            return 'calculator-widget'
        }
        static get properties() {
            return { }
        }
        ready() {
            super.ready();
        }
        static get template() {
            return html``
        }
}
customElements.define(CalculatorWidget.is, CalculatorWidget);

Properties

static get properties() {
    return {
        showwidget:{type:Boolean, value:false, reflectToAttribute: true, observer: 'showwidgetObserver' },
        collapse:{type:Boolean, value:false, reflectToAttribute: true, observer: 'collapseObserver' },
        displayvalue:{type:String},
        header_text:{type:String},
        page: {type:Number, value: 0}
    }
}

Gestures

This is done using the on-track annotation listener on the header section of the widget. This is where we want the user to be able to grab the widget.

The draggable section
handleTrack(e) {
            switch(e.detail.state) {
              case 'start':
                {
                    this.dragrect = this.$.draggable.getBoundingClientRect();
                    this.rectwidth = this.dragrect.width;
                    this.rectheight = this.dragrect.height;
                    this.windowwidth = window.innerWidth;
                    this.windowheight = window.innerHeight;
                }
                break;
              case 'track':
                  {
                    let targettop = (this.dragrect.top + e.detail.dy);
                    let targetleft = (this.dragrect.left + e.detail.dx);

                    if (targettop < 0)
                        targettop = 0;

                    if (targettop + this.rectheight > this.windowheight)
                        targettop = this.windowheight - this.rectheight;

                    if (targetleft < 0)
                        targetleft = 0;

                    if (targetleft + this.rectwidth > this.windowwidth)
                        targetleft = this.windowwidth - this.rectwidth;

                    this.$.draggable.style.top = targettop + 'px';
                    this.$.draggable.style.left = targetleft +'px';
                  }
                break;
              case 'end':
                  {
                    this.dragrect = this.$.draggable.getBoundingClientRect();
                  }
                break;
            }
        }

In the method above, we are basically getting the position of the draggable rect when the mouse event starts and adjusting the position of the element as the move event changes. The switch cases (start, track and end) help distinguish between the different ‘states’. Also, we are ensuring that the widget does not leave the screen bounds.

CSS Grid

CSS Grid of Buttons

We can define our grid of buttons using the grid templates

.kg {
    display: grid;
    grid-gap: 0.4em;
    margin: 0.5rem;
    grid-template-columns: 1fr 1fr 1fr 1fr;
    grid-template-rows: 1fr 1fr 1fr 1fr 1fr;
}

The final CalculatorWidget class

import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { GestureEventListeners } from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';

class CalculatorWidget extends GestureEventListeners(PolymerElement) {

        static get is() { return 'calculator-widget'}

        static get properties() {
            return {
                showwidget:{type:Boolean, value:false, reflectToAttribute: true, observer: 'showwidgetObserver' },
                collapse:{type:Boolean, value:false, reflectToAttribute: true, observer: 'collapseObserver' },
                displayvalue:{type:String},
                header_text:{type:String},
                page: {type:Number, value: 0}
            }
        }

        ready() {
            super.ready();
            this.displayvalue = '0';
            this.history = '';
            this.calculator = {
                firstOperand: undefined,
                waitingForSecondOperand: false,
                operator: null,
            };
        }

        inputDigit(e) {
          let digit = e.target.value;
          let { waitingForSecondOperand } = this.calculator;
          if (waitingForSecondOperand === true) {
            this.displayvalue = digit;
            this.calculator.waitingForSecondOperand = false;
          } else {
            this.displayvalue = this.displayvalue === '0' ? digit : this.displayvalue + digit;
          }
        }

        inputDecimal(e) {
            let dot = e.target.value;

        	if (this.calculator.waitingForSecondOperand === true) return;
            // If the `displayvalue` does not contain a decimal point
            if (!this.displayvalue.includes(dot)) {
                // Append the decimal point
                this.displayvalue += dot;
            }
        }

        handleOperator(e) {
            let nextOperator = e.target.value;
            let { firstOperand, operator } = this.calculator

            let inputValue = parseFloat(this.displayvalue);

            if (operator && this.calculator.waitingForSecondOperand)  {
                this.calculator.operator = nextOperator;
                return;
            }

            if (firstOperand == null) {
                this.calculator.firstOperand = inputValue;
            } else if (operator) {
                let currentValue = firstOperand || 0;
                let result = this.performCalculation(operator, currentValue, inputValue);

                if(operator === '=') {
                    this.history = '';
                } else {
                    this.history = currentValue + ' ' + operator + ' ' + inputValue;
                }

                this.displayvalue = String(result);
                this.calculator.firstOperand = result;
            }
            this.calculator.waitingForSecondOperand = true;
            this.calculator.operator = nextOperator;
        }

        resetCalculator() {
          this.history = '';
          this.displayvalue = '0';
          this.calculator.firstOperand = null;
          this.calculator.waitingForSecondOperand = false;
          this.calculator.operator = null;
        }

        performCalculation(operator, a, b)  {
          if('/'=== operator) return a / b;
          if('*'=== operator) return a * b;
          if('+'=== operator) return a + b;
          if('-'=== operator) return a - b;
          if('%'=== operator) return a / 100.00;
          if('_'=== operator) return (-1) * a;
          if('='=== operator) return b;
        };

        showwidgetObserver(newVar, oldVar) {
            if (newVar)
                this.openWidget();
            else
                this.closeWidget();
        }

        collapseObserver (newVar, oldVar) {
            if(newVar)
                this.$.calculatorkeys.classList.add("displaynone");
            else
                this.$.calculatorkeys.classList.remove("displaynone");
        }

        iconForCollapse(r) { return (r === true) ? 'lumo:plus' : 'lumo:minus'; }

        closeWidget() {
            this.$.draggable.classList.remove("animated", "fadeIn", "faster");
            this.$.draggable.classList.add("displaynone");
        }

        openWidget() {
            this.$.draggable.classList.add("animated", "fadeIn", "faster");
            this.$.draggable.classList.remove("displaynone");
        }

        toggleWidgetOpen() {
            this.showwidget = !this.showwidget;
        }

        toggleWidgetCollapse() {
            this.collapse = !this.collapse;
        }

        clearAll() {
            console.log('clearAll');
            this.resetCalculator();
        }

        handleTrack(e) {
            switch(e.detail.state) {
              case 'start':
                {
                    this.dragrect = this.$.draggable.getBoundingClientRect();
                    this.rectwidth = this.dragrect.width;
                    this.rectheight = this.dragrect.height;
                    this.windowwidth = window.innerWidth;
                    this.windowheight = window.innerHeight;
                }
                break;
              case 'track':
                  {
                    let targettop = (this.dragrect.top + e.detail.dy);
                    let targetleft = (this.dragrect.left + e.detail.dx);

                    if (targettop < 0)
                        targettop = 0;

                    if (targettop + this.rectheight > this.windowheight)
                        targettop = this.windowheight - this.rectheight;

                    if (targetleft < 0)
                        targetleft = 0;

                    if (targetleft + this.rectwidth > this.windowwidth)
                        targetleft = this.windowwidth - this.rectwidth;

                    this.$.draggable.style.top = targettop + 'px';
                    this.$.draggable.style.left = targetleft +'px';
                  }
                break;
              case 'end':
                  {
                    this.dragrect = this.$.draggable.getBoundingClientRect();
                  }
                break;
            }
        }

    static get template() {
    	return html`<style>
    	.calculator-window-content {
    	    right:0;
        	border-radius: var(--lumo-border-radius);
            display:block;
            position:fixed;
            background-color: hsl(180, 2.4%, 8%);
            width:230px;
            box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
            z-index:4000;
            overflow:hidden;
            border: 1px solid var(--lumo-contrast-5pct);
        }
        .wdgtheaderbox {
            color: white;
            line-height: 1em;
            padding: 0.3rem 0.5rem;
            cursor: grab;
            flex-shrink: 0;
            display:flex;
            justify-content:space-between;
            align-items:center;
            height: 1.5rem;
        }
        .wdgtheaderbox:active { cursor: grabbing; }
        .wdgtfooterbox {
        	line-height: 1em;
        	border-top: 1px solid var(--lumo-shade-10pct);
            padding: 10px 2px;
            flex-shrink: 0;
        }

        #dropdown { }
        #widgetholder {  }

        .iconsminus { color:orange; width:1.5em; height:1.5em; }
        .iconcross { color:orange; width:1.5em; height:1.5em; }
        .displaynone { display:none; }
        .header_text { font-weight:600; font-size: var(--lumo-font-size-s); }

        .screen > .active {
            font-size: 2.8rem;
            height: 4rem;
            color: white;
            text-align: right;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .screen > .history {
            font-size: 0.8rem;
            height: 1rem;
            color: grey;
        }

        .screen {
            padding: 0em 0.5em 0.1em 0.4em;
            margin: 0 0.5rem;
            font-family: var(--lumo-font-family);
        }

        button {
            border: 0px;
            cursor: pointer;
            font-weight: 600;
            background: #46464642;
            font-size: var(--lumo-font-size-l);
            font-family: var(--lumo-font-family);
            color: white;
            border-radius: 3rem;
            transition: ease-in-out 0.15s;
        }

        button:hover { background: #80808042; }
        button:active { background: var(--lumo-contrast-20pct); }
        button:hover, button:focus { text-decoration: none; }

        .sp1 { background-color: #a5a5a56e; }
        .sp2 { background-color: #ff9e0ab3; }
        .sp2:hover { background-color: #ffa700; }
        .all-clear:hover { background-color: var(--lumo-error-color-50pct); }
        .eq { background-color:var(--lumo-primary-color-50pct); }
        .eq:hover { background-color:var(--lumo-primary-color); }

        .kg {
            display:grid;
            grid-gap:0.4em;
            margin:0.5rem;
            grid-template-columns: 1fr 1fr 1fr 1fr;
            grid-template-rows: 1fr 1fr 1fr 1fr 1fr;
        }

        .kg::before {
            content: '';
            width: 0;
            padding-bottom: 100%;
            grid-row: 1 / 1;
            grid-column: 1 / 1;
        }

        .kg > *:first-child {
          grid-row: 1 / 1;
          grid-column: 1 / 1;
        }

        page {
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        .spanz { grid-column: 1 / 3; }
    	</style>
        <vaadin-button title='Calculator' on-click="toggleWidgetOpen" id='openbtn' theme='small tertiary icon'><feather-icon icon="grid" size='22px' color="var(--lumo-contrast-50pct)"></feather-icon></vaadin-button>
        <div class='calculator-window-content displaynone' id='draggable'>
            <div id='headerbox' class='wdgtheaderbox' on-track="handleTrack">
                <span class='header_text'>Calculator</span>
                <div>
                     <vaadin-button on-click="toggleWidgetCollapse" theme='small tertiary-inline'><iron-icon icon="[[iconForCollapse(collapse)]]"></iron-icon></vaadin-button>
                     <vaadin-button on-click="toggleWidgetOpen" theme='small tertiary-inline error'><iron-icon icon="lumo:cross"></iron-icon></vaadin-button>
                </div>
            </div>
            <div id='widgetholder'>
               <vaadin-tabs selected="{{page}}" theme="centered small" disable hidden>
                  <vaadin-tab>Calculator</vaadin-tab>
                  <vaadin-tab>Converter</vaadin-tab>
                </vaadin-tabs>
                <iron-pages selected="[[page]]">
                  <page>
                  </page>
                  <page></page>
                </iron-pages>
                <div>
                  <div class="screen">
                      <div class="history">[[history]]</div>
                      <div class="active" title=[[displayvalue]]>[[displayvalue]]</div>
                  </div>
                  <div id='calculatorkeys'>
                  <div class="kg">
                    <button class="sp1 all-clear" value="all-clear" on-click="clearAll">AC</button>
                    <button class="sp1" value="%" on-click="handleOperator">%</button>
                    <button class="sp1" value="_" on-click="handleOperator">&#8210;/+</button>
                    <button class="sp2" value="/" on-click="handleOperator">&divide;</button>

                    <button value="7" on-click="inputDigit">7</button>
                    <button value="8" on-click="inputDigit">8</button>
                    <button value="9" on-click="inputDigit">9</button>
                    <button class="sp2" value="*" on-click="handleOperator">&times;</button>

                    <button value="4" on-click="inputDigit">4</button>
                    <button value="5" on-click="inputDigit">5</button>
                    <button value="6" on-click="inputDigit">6</button>
                    <button class="sp2" value="-" on-click="handleOperator">&#8210;</button>

                    <button value="1" on-click="inputDigit">1</button>
                    <button value="2" on-click="inputDigit">2</button>
                    <button value="3" on-click="inputDigit">3</button>
                    <button class="sp2" value="+" on-click="handleOperator">&#43;</button>

                    <button class="spanz" value="0" on-click="inputDigit">0</button>
                    <button value="." on-click="inputDecimal">&#183;</button>
                    <button class="eq" value="=" on-click="handleOperator">=</button>
                  </div>
                  </div>
                </div>
            </div>
        </div>`
    }
}
customElements.define(CalculatorWidget.is, CalculatorWidget);