Skip to content

Commit

Permalink
add return values to drawString & drawStringMaxWidth (#365)
Browse files Browse the repository at this point in the history
* get rid of utf8ascii()

Make the drawString*() functions and getStringWidth() directly convert
UTF-8 on the fly if needed. This saves an extra malloc for the converted
string in most cases which then needs to be free()d and allows to count
drawn chars even for UTF-8 strings later.
Keep the utf8ascii() function to not break the API for derived classes.

* drawStringInternal: return number of chars drawn

Return the nuber of characters that was drawn. If this is less then the
string length, then the text was too long for the display. This allows
e.g. for custom word wrapping.

* drawString: return number of characters drawn

* drawStringMaxWidth: return chars written in first line

This allows do scroll easily through longer texts, by noting the number
of chars drawn in first line and then starting the text with this offset
in the next display cycle

* drawStringMaxWidth: fix UTF-8 width calculation

* add SSD1306ScrollVerticalDemo example

this shows how the return value of drawStringMaxWidth() can be used
  • Loading branch information
seife committed Mar 27, 2022
1 parent c5a8e8c commit 95e6399
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 33 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,16 +231,19 @@ void drawXbm(int16_t x, int16_t y, int16_t width, int16_t height, const uint8_t
## Text operations

``` C++
void drawString(int16_t x, int16_t y, const String &text);
// Draws a string at the given location, returns how many chars have been written
uint16_t drawString(int16_t x, int16_t y, const String &text);

// Draws a String with a maximum width at the given location.
// If the given String is wider than the specified width
// The text will be wrapped to the next line at a space or dash
void drawStringMaxWidth(int16_t x, int16_t y, int16_t maxLineWidth, const String &text);
// returns 0 if everything fits on the screen or the numbers of characters in the
// first line if not
uint16_t drawStringMaxWidth(int16_t x, int16_t y, uint16_t maxLineWidth, const String &text);

// Returns the width of the const char* with the current
// font settings
uint16_t getStringWidth(const char* text, uint16_t length);
uint16_t getStringWidth(const char* text, uint16_t length, bool utf8 = false);

// Convencience method for the const char version
uint16_t getStringWidth(const String &text);
Expand Down
91 changes: 91 additions & 0 deletions examples/SSD1306ScrollVerticalDemo/SSD1306ScrollVerticalDemo.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
The MIT License (MIT)
Copyright (c) 2022 by Stefan Seyfried
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

// Include the correct display library
// For a connection via I2C using Wire include
#include <Wire.h> // Only needed for Arduino 1.6.5 and earlier
#include "SSD1306Wire.h" // legacy include: `#include "SSD1306.h"`
// or #include "SH1106Wire.h", legacy include: `#include "SH1106.h"`
// For a connection via I2C using brzo_i2c (must be installed) include
// #include <brzo_i2c.h> // Only needed for Arduino 1.6.5 and earlier
// #include "SSD1306Brzo.h"
// #include "SH1106Brzo.h"
// For a connection via SPI include
// #include <SPI.h> // Only needed for Arduino 1.6.5 and earlier
// #include "SSD1306Spi.h"
// #include "SH1106Spi.h"

// Use the corresponding display class:

// Initialize the OLED display using SPI
// D5 -> CLK
// D7 -> MOSI (DOUT)
// D0 -> RES
// D2 -> DC
// D8 -> CS
// SSD1306Spi display(D0, D2, D8);
// or
// SH1106Spi display(D0, D2);

// Initialize the OLED display using brzo_i2c
// D3 -> SDA
// D5 -> SCL
// SSD1306Brzo display(0x3c, D3, D5);
// or
// SH1106Brzo display(0x3c, D3, D5);

// Initialize the OLED display using Wire library
SSD1306Wire display(0x3c, SDA, SCL); // ADDRESS, SDA, SCL - SDA and SCL usually populate automatically based on your board's pins_arduino.h e.g. https://github.com/esp8266/Arduino/blob/master/variants/nodemcu/pins_arduino.h
// SH1106Wire display(0x3c, SDA, SCL);

// UTF-8 sprinkled within, because it tests special conditions in the char-counting code
const String loremipsum = "Lorem ipsum dolor sit ämet, "
"consetetur sadipscing elitr, sed diam nonümy eirmöd "
"tempor invidunt ut labore et dolore mägnä aliquyam erat, "
"sed diam voluptua. At vero eos et accusam et justo duo "
"dolores et ea rebum. Stet clita kasd gubergren, no sea "
"takimata sanctus est Lorem ipsum dolor sit amet. "
"äöü-ÄÖÜ/߀é/çØ.";

void setup() {
display.init();
display.setContrast(255);
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.setFont(ArialMT_Plain_16);
display.display();
}

void loop() {
static uint16_t start_at = 0;
display.clear();
uint16_t firstline = display.drawStringMaxWidth(0, 0, 128, loremipsum.substring(start_at));
display.display();
if (firstline != 0) {
start_at += firstline;
} else {
start_at = 0;
delay(1000); // additional pause before going back to start
}
delay(1000);
}
79 changes: 54 additions & 25 deletions src/OLEDDisplay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -553,13 +553,14 @@ void OLEDDisplay::drawIco16x16(int16_t xMove, int16_t yMove, const uint8_t *ico,
}
}

void OLEDDisplay::drawStringInternal(int16_t xMove, int16_t yMove, char* text, uint16_t textLength, uint16_t textWidth) {
uint16_t OLEDDisplay::drawStringInternal(int16_t xMove, int16_t yMove, const char* text, uint16_t textLength, uint16_t textWidth, bool utf8) {
uint8_t textHeight = pgm_read_byte(fontData + HEIGHT_POS);
uint8_t firstChar = pgm_read_byte(fontData + FIRST_CHAR_POS);
uint16_t sizeOfJumpTable = pgm_read_byte(fontData + CHAR_NUM_POS) * JUMPTABLE_BYTES;

uint16_t cursorX = 0;
uint16_t cursorY = 0;
uint16_t charCount = 0;

switch (textAlignment) {
case TEXT_ALIGN_CENTER_BOTH:
Expand All @@ -576,16 +577,23 @@ void OLEDDisplay::drawStringInternal(int16_t xMove, int16_t yMove, char* text, u
}

// Don't draw anything if it is not on the screen.
if (xMove + textWidth < 0 || xMove > this->width() ) {return;}
if (yMove + textHeight < 0 || yMove > this->height()) {return;}
if (xMove + textWidth < 0 || xMove >= this->width() ) {return 0;}
if (yMove + textHeight < 0 || yMove >= this->height()) {return 0;}

for (uint16_t j = 0; j < textLength; j++) {
int16_t xPos = xMove + cursorX;
int16_t yPos = yMove + cursorY;
if (xPos > this->width())
break; // no need to continue

uint8_t code = text[j];
charCount++;

uint8_t code;
if (utf8) {
code = (this->fontTableLookupFunction)(text[j]);
if (code == 0)
continue;
} else
code = text[j];
if (code >= firstChar) {
uint8_t charCode = code - firstChar;

Expand All @@ -605,14 +613,19 @@ void OLEDDisplay::drawStringInternal(int16_t xMove, int16_t yMove, char* text, u
cursorX += currentCharWidth;
}
}
return charCount;
}


void OLEDDisplay::drawString(int16_t xMove, int16_t yMove, const String &strUser) {
uint16_t OLEDDisplay::drawString(int16_t xMove, int16_t yMove, const String &strUser) {
uint16_t lineHeight = pgm_read_byte(fontData + HEIGHT_POS);

// char* text must be freed!
char* text = utf8ascii(strUser);
char* text = strdup(strUser.c_str());
if (!text) {
DEBUG_OLEDDISPLAY("[OLEDDISPLAY][drawString] Can't allocate char array.\n");
return 0;
}

uint16_t yOffset = 0;
// If the string should be centered vertically too
Expand All @@ -627,14 +640,16 @@ void OLEDDisplay::drawString(int16_t xMove, int16_t yMove, const String &strUser
yOffset = (lb * lineHeight) / 2;
}

uint16_t charDrawn = 0;
uint16_t line = 0;
char* textPart = strtok(text,"\n");
while (textPart != NULL) {
uint16_t length = strlen(textPart);
drawStringInternal(xMove, yMove - yOffset + (line++) * lineHeight, textPart, length, getStringWidth(textPart, length));
charDrawn += drawStringInternal(xMove, yMove - yOffset + (line++) * lineHeight, textPart, length, getStringWidth(textPart, length, true), true);
textPart = strtok(NULL, "\n");
}
free(text);
return charDrawn;
}

void OLEDDisplay::drawStringf( int16_t x, int16_t y, char* buffer, String format, ... )
Expand All @@ -646,11 +661,11 @@ void OLEDDisplay::drawStringf( int16_t x, int16_t y, char* buffer, String format
drawString( x, y, buffer );
}

void OLEDDisplay::drawStringMaxWidth(int16_t xMove, int16_t yMove, uint16_t maxLineWidth, const String &strUser) {
uint16_t OLEDDisplay::drawStringMaxWidth(int16_t xMove, int16_t yMove, uint16_t maxLineWidth, const String &strUser) {
uint16_t firstChar = pgm_read_byte(fontData + FIRST_CHAR_POS);
uint16_t lineHeight = pgm_read_byte(fontData + HEIGHT_POS);

char* text = utf8ascii(strUser);
const char* text = strUser.c_str();

uint16_t length = strlen(text);
uint16_t lastDrawnPos = 0;
Expand All @@ -659,9 +674,14 @@ void OLEDDisplay::drawStringMaxWidth(int16_t xMove, int16_t yMove, uint16_t maxL

uint16_t preferredBreakpoint = 0;
uint16_t widthAtBreakpoint = 0;
uint16_t firstLineChars = 0;
uint16_t drawStringResult = 1; // later tested for 0 == error, so initialize to 1

for (uint16_t i = 0; i < length; i++) {
strWidth += pgm_read_byte(fontData + JUMPTABLE_START + (text[i] - firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH);
char c = (this->fontTableLookupFunction)(text[i]);
if (c == 0)
continue;
strWidth += pgm_read_byte(fontData + JUMPTABLE_START + (c - firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH);

// Always try to break on a space, dash or slash
if (text[i] == ' ' || text[i]== '-' || text[i] == '/') {
Expand All @@ -674,33 +694,45 @@ void OLEDDisplay::drawStringMaxWidth(int16_t xMove, int16_t yMove, uint16_t maxL
preferredBreakpoint = i;
widthAtBreakpoint = strWidth;
}
drawStringInternal(xMove, yMove + (lineNumber++) * lineHeight , &text[lastDrawnPos], preferredBreakpoint - lastDrawnPos, widthAtBreakpoint);
drawStringResult = drawStringInternal(xMove, yMove + (lineNumber++) * lineHeight , &text[lastDrawnPos], preferredBreakpoint - lastDrawnPos, widthAtBreakpoint, true);
if (firstLineChars == 0)
firstLineChars = preferredBreakpoint;
lastDrawnPos = preferredBreakpoint;
// It is possible that we did not draw all letters to i so we need
// to account for the width of the chars from `i - preferredBreakpoint`
// by calculating the width we did not draw yet.
strWidth = strWidth - widthAtBreakpoint;
preferredBreakpoint = 0;
if (drawStringResult == 0) // we are past the display already?
break;
}
}

// Draw last part if needed
if (lastDrawnPos < length) {
drawStringInternal(xMove, yMove + lineNumber * lineHeight , &text[lastDrawnPos], length - lastDrawnPos, getStringWidth(&text[lastDrawnPos], length - lastDrawnPos));
if (drawStringResult != 0 && lastDrawnPos < length) {
drawStringResult = drawStringInternal(xMove, yMove + (lineNumber++) * lineHeight , &text[lastDrawnPos], length - lastDrawnPos, getStringWidth(&text[lastDrawnPos], length - lastDrawnPos, true), true);
}

free(text);
if (drawStringResult == 0 || (yMove + lineNumber * lineHeight) >= this->height()) // text did not fit on screen
return firstLineChars;
return 0; // everything was drawn
}

uint16_t OLEDDisplay::getStringWidth(const char* text, uint16_t length) {
uint16_t OLEDDisplay::getStringWidth(const char* text, uint16_t length, bool utf8) {
uint16_t firstChar = pgm_read_byte(fontData + FIRST_CHAR_POS);

uint16_t stringWidth = 0;
uint16_t maxWidth = 0;

while (length--) {
stringWidth += pgm_read_byte(fontData + JUMPTABLE_START + (text[length] - firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH);
if (text[length] == 10) {
for (uint16_t i = 0; i < length; i++) {
char c = text[i];
if (utf8) {
c = (this->fontTableLookupFunction)(c);
if (c == 0)
continue;
}
stringWidth += pgm_read_byte(fontData + JUMPTABLE_START + (c - firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH);
if (c == 10) {
maxWidth = max(maxWidth, stringWidth);
stringWidth = 0;
}
Expand All @@ -710,10 +742,7 @@ uint16_t OLEDDisplay::getStringWidth(const char* text, uint16_t length) {
}

uint16_t OLEDDisplay::getStringWidth(const String &strUser) {
char* text = utf8ascii(strUser);
uint16_t length = strlen(text);
uint16_t width = getStringWidth(text, length);
free(text);
uint16_t width = getStringWidth(strUser.c_str(), strUser.length());
return width;
}

Expand Down Expand Up @@ -806,7 +835,7 @@ void OLEDDisplay::drawLogBuffer(uint16_t xMove, uint16_t yMove) {
length++;
// Draw string on line `line` from lastPos to length
// Passing 0 as the lenght because we are in TEXT_ALIGN_LEFT
drawStringInternal(xMove, yMove + (line++) * lineHeight, &this->logBuffer[lastPos], length, 0);
drawStringInternal(xMove, yMove + (line++) * lineHeight, &this->logBuffer[lastPos], length, 0, false);
// Remember last pos
lastPos = i;
// Reset length
Expand All @@ -818,7 +847,7 @@ void OLEDDisplay::drawLogBuffer(uint16_t xMove, uint16_t yMove) {
}
// Draw the remaining string
if (length > 0) {
drawStringInternal(xMove, yMove + line * lineHeight, &this->logBuffer[lastPos], length, 0);
drawStringInternal(xMove, yMove + line * lineHeight, &this->logBuffer[lastPos], length, 0, false);
}
}

Expand Down
12 changes: 7 additions & 5 deletions src/OLEDDisplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -242,20 +242,22 @@ class OLEDDisplay : public Stream {

/* Text functions */

// Draws a string at the given location
void drawString(int16_t x, int16_t y, const String &text);
// Draws a string at the given location, returns how many chars have been written
uint16_t drawString(int16_t x, int16_t y, const String &text);

// Draws a formatted string (like printf) at the given location
void drawStringf(int16_t x, int16_t y, char* buffer, String format, ... );

// Draws a String with a maximum width at the given location.
// If the given String is wider than the specified width
// The text will be wrapped to the next line at a space or dash
void drawStringMaxWidth(int16_t x, int16_t y, uint16_t maxLineWidth, const String &text);
// returns 0 if everything fits on the screen or the numbers of characters in the
// first line if not
uint16_t drawStringMaxWidth(int16_t x, int16_t y, uint16_t maxLineWidth, const String &text);

// Returns the width of the const char* with the current
// font settings
uint16_t getStringWidth(const char* text, uint16_t length);
uint16_t getStringWidth(const char* text, uint16_t length, bool utf8 = false);

// Convencience method for the const char version
uint16_t getStringWidth(const String &text);
Expand Down Expand Up @@ -382,7 +384,7 @@ class OLEDDisplay : public Stream {

void inline drawInternal(int16_t xMove, int16_t yMove, int16_t width, int16_t height, const uint8_t *data, uint16_t offset, uint16_t bytesInData) __attribute__((always_inline));

void drawStringInternal(int16_t xMove, int16_t yMove, char* text, uint16_t textLength, uint16_t textWidth);
uint16_t drawStringInternal(int16_t xMove, int16_t yMove, const char* text, uint16_t textLength, uint16_t textWidth, bool utf8);

FontTableLookupFunction fontTableLookupFunction;
};
Expand Down

0 comments on commit 95e6399

Please sign in to comment.