From ef11865d2490356158811d879d010a2c9dd5d31b Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Sat, 15 Feb 2014 21:13:44 -0800 Subject: [PATCH] implement text overlay support --- examples/overlaysExample.js | 21 ++++++++ interface/src/Util.h | 9 ---- interface/src/ui/ImageOverlay.cpp | 69 +----------------------- interface/src/ui/ImageOverlay.h | 32 ++--------- interface/src/ui/Overlay.cpp | 88 +++++++++++++++++++++++++++++++ interface/src/ui/Overlay.h | 64 ++++++++++++++++++++++ interface/src/ui/Overlays.cpp | 29 ++++++---- interface/src/ui/Overlays.h | 4 +- interface/src/ui/TextOverlay.cpp | 80 ++++++++++++++++++++++++++++ interface/src/ui/TextOverlay.h | 58 ++++++++++++++++++++ interface/src/ui/TextRenderer.cpp | 34 +++++++++--- interface/src/ui/TextRenderer.h | 16 +++++- 12 files changed, 382 insertions(+), 122 deletions(-) create mode 100644 interface/src/ui/Overlay.cpp create mode 100644 interface/src/ui/Overlay.h create mode 100644 interface/src/ui/TextOverlay.cpp create mode 100644 interface/src/ui/TextOverlay.h diff --git a/examples/overlaysExample.js b/examples/overlaysExample.js index 778de0421e..79cd65df68 100644 --- a/examples/overlaysExample.js +++ b/examples/overlaysExample.js @@ -44,6 +44,19 @@ for (s = 0; s < numberOfSwatches; s++) { }); } +var text = Overlays.addOverlay("text", { + x: 200, + y: 100, + width: 150, + height: 50, + backgroundColor: { red: 0, green: 0, blue: 0}, + textColor: { red: 255, green: 0, blue: 0}, + topMargin: 4, + leftMargin: 4, + alpha: 0.7, + text: "Here is some text.\nAnd a second line." + }); + var toolA = Overlays.addOverlay("image", { x: 100, y: 100, @@ -97,6 +110,7 @@ function scriptEnding() { } Overlays.deleteOverlay(thumb); Overlays.deleteOverlay(slider); + Overlays.deleteOverlay(text); } Script.scriptEnding.connect(scriptEnding); @@ -132,10 +146,14 @@ function mouseMoveEvent(event) { } } function mousePressEvent(event) { + var clickedText = false; var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); if (clickedOverlay == thumb) { movingSlider = true; thumbClickOffsetX = event.x - thumbX; + } else if (clickedOverlay == text) { + Overlays.editOverlay(text, { text: "you clicked here:\n " + event.x + "," + event.y } ); + clickedText = true; } else { for (s = 0; s < numberOfSwatches; s++) { if (clickedOverlay == swatches[s]) { @@ -145,6 +163,9 @@ function mousePressEvent(event) { } } } + if (!clickedText) { + Overlays.editOverlay(text, { text: "you didn't click here" } ); + } } function mouseReleaseEvent(event) { diff --git a/interface/src/Util.h b/interface/src/Util.h index 09d1fa0484..0c762ccd79 100644 --- a/interface/src/Util.h +++ b/interface/src/Util.h @@ -19,15 +19,6 @@ #include #include -// the standard sans serif font family -#define SANS_FONT_FAMILY "Helvetica" - -// the standard mono font family -#define MONO_FONT_FAMILY "Courier" - -// the Inconsolata font family -#define INCONSOLATA_FONT_FAMILY "Inconsolata" - void eulerToOrthonormals(glm::vec3 * angles, glm::vec3 * fwd, glm::vec3 * left, glm::vec3 * up); float azimuth_to(glm::vec3 head_pos, glm::vec3 source_pos); diff --git a/interface/src/ui/ImageOverlay.cpp b/interface/src/ui/ImageOverlay.cpp index 5d7d7efcc0..d36099cffc 100644 --- a/interface/src/ui/ImageOverlay.cpp +++ b/interface/src/ui/ImageOverlay.cpp @@ -14,23 +14,13 @@ #include ImageOverlay::ImageOverlay() : - _parent(NULL), _textureID(0), - _alpha(DEFAULT_ALPHA), - _backgroundColor(DEFAULT_BACKGROUND_COLOR), - _visible(true), _renderImage(false), _textureBound(false), _wantClipFromImage(false) { } -void ImageOverlay::init(QGLWidget* parent) { - qDebug() << "ImageOverlay::init() parent=" << parent; - _parent = parent; -} - - ImageOverlay::~ImageOverlay() { if (_parent && _textureID) { // do we need to call this? @@ -114,44 +104,9 @@ void ImageOverlay::render() { } } -// TODO: handle only setting the included values... void ImageOverlay::setProperties(const QScriptValue& properties) { - //qDebug() << "ImageOverlay::setProperties()... properties=" << &properties; - QScriptValue bounds = properties.property("bounds"); - if (bounds.isValid()) { - QRect boundsRect; - boundsRect.setX(bounds.property("x").toVariant().toInt()); - boundsRect.setY(bounds.property("y").toVariant().toInt()); - boundsRect.setWidth(bounds.property("width").toVariant().toInt()); - boundsRect.setHeight(bounds.property("height").toVariant().toInt()); - setBounds(boundsRect); - } else { - QRect oldBounds = getBounds(); - QRect newBounds = oldBounds; - - if (properties.property("x").isValid()) { - newBounds.setX(properties.property("x").toVariant().toInt()); - } else { - newBounds.setX(oldBounds.x()); - } - if (properties.property("y").isValid()) { - newBounds.setY(properties.property("y").toVariant().toInt()); - } else { - newBounds.setY(oldBounds.y()); - } - if (properties.property("width").isValid()) { - newBounds.setWidth(properties.property("width").toVariant().toInt()); - } else { - newBounds.setWidth(oldBounds.width()); - } - if (properties.property("height").isValid()) { - newBounds.setHeight(properties.property("height").toVariant().toInt()); - } else { - newBounds.setHeight(oldBounds.height()); - } - setBounds(newBounds); - //qDebug() << "set bounds to " << getBounds(); - } + Overlay::setProperties(properties); + QScriptValue subImageBounds = properties.property("subImage"); if (subImageBounds.isValid()) { QRect oldSubImageRect = _fromImage; @@ -183,26 +138,6 @@ void ImageOverlay::setProperties(const QScriptValue& properties) { if (imageURL.isValid()) { setImageURL(imageURL.toVariant().toString()); } - - QScriptValue color = properties.property("backgroundColor"); - if (color.isValid()) { - QScriptValue red = color.property("red"); - QScriptValue green = color.property("green"); - QScriptValue blue = color.property("blue"); - if (red.isValid() && green.isValid() && blue.isValid()) { - _backgroundColor.red = red.toVariant().toInt(); - _backgroundColor.green = green.toVariant().toInt(); - _backgroundColor.blue = blue.toVariant().toInt(); - } - } - - if (properties.property("alpha").isValid()) { - setAlpha(properties.property("alpha").toVariant().toFloat()); - } - - if (properties.property("visible").isValid()) { - setVisible(properties.property("visible").toVariant().toBool()); - } } diff --git a/interface/src/ui/ImageOverlay.h b/interface/src/ui/ImageOverlay.h index de2dcb7693..be7d8cf5d8 100644 --- a/interface/src/ui/ImageOverlay.h +++ b/interface/src/ui/ImageOverlay.h @@ -22,43 +22,24 @@ #include -const xColor DEFAULT_BACKGROUND_COLOR = { 255, 255, 255 }; -const float DEFAULT_ALPHA = 0.7f; +#include "Overlay.h" -class ImageOverlay : QObject { +class ImageOverlay : public Overlay { Q_OBJECT public: ImageOverlay(); ~ImageOverlay(); - void init(QGLWidget* parent); - void render(); + virtual void render(); -//public slots: // getters - bool getVisible() const { return _visible; } - int getX() const { return _bounds.x(); } - int getY() const { return _bounds.y(); } - int getWidth() const { return _bounds.width(); } - int getHeight() const { return _bounds.height(); } - const QRect& getBounds() const { return _bounds; } const QRect& getClipFromSource() const { return _fromImage; } - const xColor& getBackgroundColor() const { return _backgroundColor; } - float getAlpha() const { return _alpha; } const QUrl& getImageURL() const { return _imageURL; } // setters - void setVisible(bool visible) { _visible = visible; } - void setX(int x) { _bounds.setX(x); } - void setY(int y) { _bounds.setY(y); } - void setWidth(int width) { _bounds.setWidth(width); } - void setHeight(int height) { _bounds.setHeight(height); } - void setBounds(const QRect& bounds) { _bounds = bounds; } void setClipFromSource(const QRect& bounds) { _fromImage = bounds; _wantClipFromImage = true; } - void setBackgroundColor(const xColor& color) { _backgroundColor = color; } - void setAlpha(float alpha) { _alpha = alpha; } void setImageURL(const QUrl& url); - void setProperties(const QScriptValue& properties); + virtual void setProperties(const QScriptValue& properties); private slots: void replyFinished(QNetworkReply* reply); // we actually want to hide this... @@ -66,14 +47,9 @@ private slots: private: QUrl _imageURL; - QGLWidget* _parent; QImage _textureImage; GLuint _textureID; - QRect _bounds; // where on the screen to draw QRect _fromImage; // where from in the image to sample - float _alpha; - xColor _backgroundColor; - bool _visible; // should the overlay be drawn at all bool _renderImage; // is there an image associated with this overlay, or is it just a colored rectangle bool _textureBound; // has the texture been bound bool _wantClipFromImage; diff --git a/interface/src/ui/Overlay.cpp b/interface/src/ui/Overlay.cpp new file mode 100644 index 0000000000..fa54844477 --- /dev/null +++ b/interface/src/ui/Overlay.cpp @@ -0,0 +1,88 @@ +// +// Overlay.cpp +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + + +#include "Overlay.h" + +#include +#include +#include +#include + +Overlay::Overlay() : + _parent(NULL), + _alpha(DEFAULT_ALPHA), + _backgroundColor(DEFAULT_BACKGROUND_COLOR), + _visible(true) +{ +} + +void Overlay::init(QGLWidget* parent) { + _parent = parent; +} + + +Overlay::~Overlay() { +} + +void Overlay::setProperties(const QScriptValue& properties) { + QScriptValue bounds = properties.property("bounds"); + if (bounds.isValid()) { + QRect boundsRect; + boundsRect.setX(bounds.property("x").toVariant().toInt()); + boundsRect.setY(bounds.property("y").toVariant().toInt()); + boundsRect.setWidth(bounds.property("width").toVariant().toInt()); + boundsRect.setHeight(bounds.property("height").toVariant().toInt()); + setBounds(boundsRect); + } else { + QRect oldBounds = getBounds(); + QRect newBounds = oldBounds; + + if (properties.property("x").isValid()) { + newBounds.setX(properties.property("x").toVariant().toInt()); + } else { + newBounds.setX(oldBounds.x()); + } + if (properties.property("y").isValid()) { + newBounds.setY(properties.property("y").toVariant().toInt()); + } else { + newBounds.setY(oldBounds.y()); + } + if (properties.property("width").isValid()) { + newBounds.setWidth(properties.property("width").toVariant().toInt()); + } else { + newBounds.setWidth(oldBounds.width()); + } + if (properties.property("height").isValid()) { + newBounds.setHeight(properties.property("height").toVariant().toInt()); + } else { + newBounds.setHeight(oldBounds.height()); + } + setBounds(newBounds); + //qDebug() << "set bounds to " << getBounds(); + } + + QScriptValue color = properties.property("backgroundColor"); + if (color.isValid()) { + QScriptValue red = color.property("red"); + QScriptValue green = color.property("green"); + QScriptValue blue = color.property("blue"); + if (red.isValid() && green.isValid() && blue.isValid()) { + _backgroundColor.red = red.toVariant().toInt(); + _backgroundColor.green = green.toVariant().toInt(); + _backgroundColor.blue = blue.toVariant().toInt(); + } + } + + if (properties.property("alpha").isValid()) { + setAlpha(properties.property("alpha").toVariant().toFloat()); + } + + if (properties.property("visible").isValid()) { + setVisible(properties.property("visible").toVariant().toBool()); + } +} diff --git a/interface/src/ui/Overlay.h b/interface/src/ui/Overlay.h new file mode 100644 index 0000000000..ce63fdaba5 --- /dev/null +++ b/interface/src/ui/Overlay.h @@ -0,0 +1,64 @@ +// +// Overlay.h +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__Overlay__ +#define __interface__Overlay__ + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include +#include +#include + +#include // for xColor + +const xColor DEFAULT_BACKGROUND_COLOR = { 255, 255, 255 }; +const float DEFAULT_ALPHA = 0.7f; + +class Overlay : public QObject { + Q_OBJECT + +public: + Overlay(); + ~Overlay(); + void init(QGLWidget* parent); + virtual void render() = 0; + + // getters + bool getVisible() const { return _visible; } + int getX() const { return _bounds.x(); } + int getY() const { return _bounds.y(); } + int getWidth() const { return _bounds.width(); } + int getHeight() const { return _bounds.height(); } + const QRect& getBounds() const { return _bounds; } + const xColor& getBackgroundColor() const { return _backgroundColor; } + float getAlpha() const { return _alpha; } + + // setters + void setVisible(bool visible) { _visible = visible; } + void setX(int x) { _bounds.setX(x); } + void setY(int y) { _bounds.setY(y); } + void setWidth(int width) { _bounds.setWidth(width); } + void setHeight(int height) { _bounds.setHeight(height); } + void setBounds(const QRect& bounds) { _bounds = bounds; } + void setBackgroundColor(const xColor& color) { _backgroundColor = color; } + void setAlpha(float alpha) { _alpha = alpha; } + + virtual void setProperties(const QScriptValue& properties); + +protected: + QGLWidget* _parent; + QRect _bounds; // where on the screen to draw + float _alpha; + xColor _backgroundColor; + bool _visible; // should the overlay be drawn at all +}; + + +#endif /* defined(__interface__Overlay__) */ diff --git a/interface/src/ui/Overlays.cpp b/interface/src/ui/Overlays.cpp index 5c19dc5529..2f31e7e8c7 100644 --- a/interface/src/ui/Overlays.cpp +++ b/interface/src/ui/Overlays.cpp @@ -7,6 +7,9 @@ #include "Overlays.h" +#include "ImageOverlay.h" +#include "TextOverlay.h" + unsigned int Overlays::_nextOverlayID = 1; @@ -21,7 +24,7 @@ void Overlays::init(QGLWidget* parent) { } void Overlays::render() { - foreach(ImageOverlay* thisOverlay, _imageOverlays) { + foreach(Overlay* thisOverlay, _overlays) { thisOverlay->render(); } } @@ -36,36 +39,44 @@ unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& prope ImageOverlay* thisOverlay = new ImageOverlay(); thisOverlay->init(_parent); thisOverlay->setProperties(properties); - _imageOverlays[thisID] = thisOverlay; + _overlays[thisID] = thisOverlay; + } else if (type == "text") { + thisID = _nextOverlayID; + _nextOverlayID++; + TextOverlay* thisOverlay = new TextOverlay(); + thisOverlay->init(_parent); + thisOverlay->setProperties(properties); + _overlays[thisID] = thisOverlay; } + return thisID; } // TODO: make multi-threaded safe bool Overlays::editOverlay(unsigned int id, const QScriptValue& properties) { - if (!_imageOverlays.contains(id)) { + if (!_overlays.contains(id)) { return false; } - ImageOverlay* thisOverlay = _imageOverlays[id]; + Overlay* thisOverlay = _overlays[id]; thisOverlay->setProperties(properties); return true; } // TODO: make multi-threaded safe void Overlays::deleteOverlay(unsigned int id) { - if (_imageOverlays.contains(id)) { - _imageOverlays.erase(_imageOverlays.find(id)); + if (_overlays.contains(id)) { + _overlays.erase(_overlays.find(id)); } } unsigned int Overlays::getOverlayAtPoint(const glm::vec2& point) { - QMapIterator i(_imageOverlays); + QMapIterator i(_overlays); i.toBack(); while (i.hasPrevious()) { i.previous(); unsigned int thisID = i.key(); - ImageOverlay* thisOverlay = i.value(); - if (thisOverlay->getBounds().contains(point.x, point.y, false)) { + Overlay* thisOverlay = i.value(); + if (thisOverlay->getVisible() && thisOverlay->getBounds().contains(point.x, point.y, false)) { return thisID; } } diff --git a/interface/src/ui/Overlays.h b/interface/src/ui/Overlays.h index 84e4338a72..e39949d2c9 100644 --- a/interface/src/ui/Overlays.h +++ b/interface/src/ui/Overlays.h @@ -10,7 +10,7 @@ #include -#include "ImageOverlay.h" +#include "Overlay.h" class Overlays : public QObject { Q_OBJECT @@ -35,7 +35,7 @@ public slots: unsigned int getOverlayAtPoint(const glm::vec2& point); private: - QMap _imageOverlays; + QMap _overlays; static unsigned int _nextOverlayID; QGLWidget* _parent; }; diff --git a/interface/src/ui/TextOverlay.cpp b/interface/src/ui/TextOverlay.cpp new file mode 100644 index 0000000000..aec9f641c5 --- /dev/null +++ b/interface/src/ui/TextOverlay.cpp @@ -0,0 +1,80 @@ +// +// TextOverlay.cpp +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + + +#include +#include + +#include "TextOverlay.h" +#include "TextRenderer.h" + +TextOverlay::TextOverlay() : + _leftMargin(DEFAULT_MARGIN), + _topMargin(DEFAULT_MARGIN) +{ +} + +TextOverlay::~TextOverlay() { +} + +void TextOverlay::render() { + if (!_visible) { + return; // do nothing if we're not visible + } + + const float MAX_COLOR = 255; + glColor4f(_backgroundColor.red / MAX_COLOR, _backgroundColor.green / MAX_COLOR, _backgroundColor.blue / MAX_COLOR, _alpha); + + glBegin(GL_QUADS); + glVertex2f(_bounds.left(), _bounds.top()); + glVertex2f(_bounds.right(), _bounds.top()); + glVertex2f(_bounds.right(), _bounds.bottom()); + glVertex2f(_bounds.left(), _bounds.bottom()); + glEnd(); + + //TextRenderer(const char* family, int pointSize = -1, int weight = -1, bool italic = false, + // EffectType effect = NO_EFFECT, int effectThickness = 1); + + TextRenderer textRenderer(SANS_FONT_FAMILY, 11, 50); + const int leftAdjust = -1; // required to make text render relative to left edge of bounds + const int topAdjust = -2; // required to make text render relative to top edge of bounds + int x = _bounds.left() + _leftMargin + leftAdjust; + int y = _bounds.top() + _topMargin + topAdjust; + + glColor3f(1.0f, 1.0f, 1.0f); + QStringList lines = _text.split("\n"); + int lineOffset = 0; + foreach(QString thisLine, lines) { + if (lineOffset == 0) { + lineOffset = textRenderer.calculateHeight(qPrintable(thisLine)); + } + lineOffset += textRenderer.draw(x, y + lineOffset, qPrintable(thisLine)); + + const int lineGap = 2; + lineOffset += lineGap; + } + +} + +void TextOverlay::setProperties(const QScriptValue& properties) { + Overlay::setProperties(properties); + + QScriptValue text = properties.property("text"); + if (text.isValid()) { + setText(text.toVariant().toString()); + } + + if (properties.property("leftMargin").isValid()) { + setLeftMargin(properties.property("leftMargin").toVariant().toInt()); + } + + if (properties.property("topMargin").isValid()) { + setTopMargin(properties.property("topMargin").toVariant().toInt()); + } +} + + diff --git a/interface/src/ui/TextOverlay.h b/interface/src/ui/TextOverlay.h new file mode 100644 index 0000000000..323116eccf --- /dev/null +++ b/interface/src/ui/TextOverlay.h @@ -0,0 +1,58 @@ +// +// TextOverlay.h +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__TextOverlay__ +#define __interface__TextOverlay__ + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "Overlay.h" + +const int DEFAULT_MARGIN = 10; + +class TextOverlay : public Overlay { + Q_OBJECT + +public: + TextOverlay(); + ~TextOverlay(); + virtual void render(); + + // getters + const QString& getText() const { return _text; } + int getLeftMargin() const { return _leftMargin; } + int getTopMargin() const { return _topMargin; } + + // setters + void setText(const QString& text) { _text = text; } + void setLeftMargin(int margin) { _leftMargin = margin; } + void setTopMargin(int margin) { _topMargin = margin; } + + virtual void setProperties(const QScriptValue& properties); + +private: + + QString _text; + int _leftMargin; + int _topMargin; + +}; + + +#endif /* defined(__interface__TextOverlay__) */ diff --git a/interface/src/ui/TextRenderer.cpp b/interface/src/ui/TextRenderer.cpp index 65056799e2..cacd730fd6 100644 --- a/interface/src/ui/TextRenderer.cpp +++ b/interface/src/ui/TextRenderer.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include "InterfaceConfig.h" #include "TextRenderer.h" @@ -30,10 +32,25 @@ TextRenderer::~TextRenderer() { glDeleteTextures(_allTextureIDs.size(), _allTextureIDs.constData()); } -void TextRenderer::draw(int x, int y, const char* str) { +int TextRenderer::calculateHeight(const char* str) { + int maxHeight = 0; + for (const char* ch = str; *ch != 0; ch++) { + const Glyph& glyph = getGlyph(*ch); + if (glyph.textureID() == 0) { + continue; + } + + if (glyph.bounds().height() > maxHeight) { + maxHeight = glyph.bounds().height(); + } + } + return maxHeight; +} +int TextRenderer::draw(int x, int y, const char* str) { glEnable(GL_TEXTURE_2D); + int maxHeight = 0; for (const char* ch = str; *ch != 0; ch++) { const Glyph& glyph = getGlyph(*ch); if (glyph.textureID() == 0) { @@ -41,19 +58,23 @@ void TextRenderer::draw(int x, int y, const char* str) { continue; } + if (glyph.bounds().height() > maxHeight) { + maxHeight = glyph.bounds().height(); + } + glBindTexture(GL_TEXTURE_2D, glyph.textureID()); - + int left = x + glyph.bounds().x(); int right = x + glyph.bounds().x() + glyph.bounds().width(); int bottom = y + glyph.bounds().y(); int top = y + glyph.bounds().y() + glyph.bounds().height(); - + float scale = 1.0 / IMAGE_SIZE; float ls = glyph.location().x() * scale; float rs = (glyph.location().x() + glyph.bounds().width()) * scale; float bt = glyph.location().y() * scale; float tt = (glyph.location().y() + glyph.bounds().height()) * scale; - + glBegin(GL_QUADS); glTexCoord2f(ls, bt); glVertex2f(left, bottom); @@ -64,12 +85,13 @@ void TextRenderer::draw(int x, int y, const char* str) { glTexCoord2f(ls, tt); glVertex2f(left, top); glEnd(); - + x += glyph.width(); } - glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); + + return maxHeight; } int TextRenderer::computeWidth(char ch) diff --git a/interface/src/ui/TextRenderer.h b/interface/src/ui/TextRenderer.h index ff484066d8..d6c24c1ce8 100644 --- a/interface/src/ui/TextRenderer.h +++ b/interface/src/ui/TextRenderer.h @@ -20,6 +20,16 @@ // a special "character" that renders as a solid block const char SOLID_BLOCK_CHAR = 127; +// the standard sans serif font family +#define SANS_FONT_FAMILY "Helvetica" + +// the standard mono font family +#define MONO_FONT_FAMILY "Courier" + +// the Inconsolata font family +#define INCONSOLATA_FONT_FAMILY "Inconsolata" + + class Glyph; class TextRenderer { @@ -33,7 +43,11 @@ public: const QFontMetrics& metrics() const { return _metrics; } - void draw(int x, int y, const char* str); + // returns the height of the tallest character + int calculateHeight(const char* str); + + // also returns the height of the tallest character + int draw(int x, int y, const char* str); int computeWidth(char ch); int computeWidth(const char* str);