Skip to content

Instantly share code, notes, and snippets.

@grorg
Created January 13, 2023 20:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grorg/a2ff323d3aa059b4a4fb194bde8e40d6 to your computer and use it in GitHub Desktop.
Save grorg/a2ff323d3aa059b4a4fb194bde8e40d6 to your computer and use it in GitHub Desktop.
// TextMeasurement.cpp
//
// Originally taken from Cameron McCormack's text_measurement.cpp
// https://gist.github.com/heycam/942f621ff9def88af92ddc3ec2d59a03
//
// ACME Browser Corporation is writing a brand new browser engine! One of the
// main responsibilities of a browser is to display text to the user, and this
// text must be wrapped to the width of the browser window so that the user
// is not scrolling back and forth all the time.
//
// One of ACME's junior engineers has taken a first attempt at writing some text
// measurement and wrapping functionality. The two main functions below are
// measureText and wrapText. They've written some tests, and it looks like
// the code is working! But they would appreciate review of their code.
//
// Please take a read through the code, compile and run it, and try to get an
// understanding of how it works. During the interview, we'll have a
// (Web-based) live coding session to refactor and improve the code.
//
// This should compile with any C++17 compiler, including various
// online tools (such as https://godbolt.org).
//
// For example, on macOS, with Xcode or the command-line tools installed:
// $ clang++ -std=c++17 -o TextMeasurement TextMeasurement.cpp
#include <cassert>
#include <string>
#include <vector>
#include <cstring>
#include <iostream>
#include <iomanip>
using namespace std;
// ---- Main text measurement code ----
// A single character in a Font and its width.
struct Character {
char character; // 0 means the default character!
float width;
};
// A Font is a list of characters and their widths.
class Font {
public:
Font(const char* fontName) {
assert(fontName != nullptr);
name = fontName;
m_count = 0;
}
void setDefaultCharacterWidth(float w);
void setCharacterWidth(char c, float w);
void setCharacterWidths(const char* s, float w);
int getCount() { return m_count; }
Character get(int i) { return m_characters[i]; }
std::string name;
private:
Character* m_characters;
int m_count;
};
void Font::setDefaultCharacterWidth(float w) {
if (m_count == 0) {
// can't have more than 256 char values
m_characters = new Character[256];
}
m_characters[m_count].character = 0;
m_characters[m_count].width = w;
m_count++;
}
void Font::setCharacterWidth(char c, float w) {
if (m_count == 0)
{
// can't have more than 256 char values
m_characters = new Character[256];
}
m_characters[m_count].character = c;
m_characters[m_count].width = w;
m_count++;
}
void Font::setCharacterWidths(const char* s, float w) {
for ( int i = 0; i < strlen(s); i++) {
setCharacterWidth(s[i], w);
}
}
float measureText(Font f, std::string s) {
if (s.length() == 0) {
return 0;
}
float total_width = 0;
for (int i = 0; i < s.length(); i++) {
float w;
bool found = false;
for (int j = 0; j < f.getCount(); j++) {
if (f.get(j).character == s[i]) {
w = f.get(j).width;
found = true;
}
}
if (!found) {
// look up the default width
for (int j = 0; j < f.getCount(); j++) {
if (f.get(j).character == 0) {
w = f.get(j).width;
}
}
}
total_width = total_width + w;
}
return total_width;
}
vector<string> wrapText(Font f, std::string s, float lineWidth) {
vector<string> result;
int lineStart = 0;
int lineEnd = 0;
while (true) {
float w = measureText(f, s.substr(lineStart, lineEnd - lineStart));
if (w > lineWidth) {
// too many words on the line. go back to find the last space.
while (s[lineEnd - 1] != ' ') {
lineEnd = lineEnd - 1;
}
lineEnd = lineEnd - 1; // remove the space
result.push_back(s.substr(lineStart, lineEnd - lineStart));
lineEnd++; // but don't include the space on the next line
lineStart = lineEnd;
} else {
lineEnd = lineEnd + 1;
}
if (lineEnd >= s.length()) {
// the final line
result.push_back(s.substr(lineStart, s.length() - lineStart));
break;
}
}
return result;
}
// ---- Testing framework ----
std::ostream& operator<<(std::ostream& os, const std::vector<string>& a) {
os << '[';
for (int i = 0; i < a.size(); i++) {
if (i > 0) {
os << ", ";
}
os << '\'' << a[i] << '\'';
}
return os << ']';
}
void testMeasureText(const Font& f, std::string s, float expected) {
float actual = measureText(f, s);
bool pass = actual == expected;
cout << (pass ? "PASS" : "FAIL")
<< ": measureText(" << f.name << ", \"" << s << "\") -- got "
<< actual;
if (!pass) {
cout << ", expected " << expected;
}
cout << '\n';
}
void testWrapText(const Font& f, std::string s, float lineWidth, std::vector<string> expected) {
std::vector<string> actual = wrapText(f, s, lineWidth);
bool pass = actual == expected;
cout << (pass ? "PASS" : "FAIL")
<< ": wrapText(" << f.name << ", \"" << s << "\", " << lineWidth
<< ") -- got " << actual;
if (!pass) {
cout << ", expected " << expected;
}
cout << '\n';
}
// ---- Tests ----
void runTests() {
cout << "Running tests...\n\n";
// All characters in this font are 10px wide.
Font mono("Monospace");
mono.setDefaultCharacterWidth(10);
// Most characters in this font are 8px wide, but they can range from
// 4px to 14px.
Font prop("Proportional");
prop.setCharacterWidths(" !'(),-.:;IJ[]fijl", 4);
prop.setCharacterWidths("\"/?\\_crstz{}", 6);
prop.setCharacterWidths("#&ABCDGHNU", 10);
prop.setCharacterWidths("%@MOQw", 12);
prop.setCharacterWidths("Wm", 14);
prop.setDefaultCharacterWidth(8);
// Tests.
testMeasureText(mono, "Some text", 9 * 10);
testMeasureText(prop, "Some text", 8 + 8 + 14 + 8 + 4 + 6 + 8 + 8 + 6);
testWrapText(mono, "Some text to wrap", 100, { "Some text", "to wrap" });
testWrapText(prop, "Some text to wrap", 100, { "Some text to", "wrap" });
cout << "\nFinished.\n";
}
int main() {
runTests();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment