mirror of
https://github.com/love2d/love.git
synced 2026-08-17 19:23:38 +02:00
Rewrote Font:getWrap (and thus love.graphics.printf's logic) to behave better. Resolves issue #799.
It now wraps text at the specified wrap limit even when there are no spaces in a line, and has improved performance.
This commit is contained in:
@@ -346,7 +346,7 @@ float Font::getHeight() const
|
|||||||
return (float) height;
|
return (float) height;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Font::DrawCommand> Font::generateVertices(const std::string &text, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector offset, TextInfo *info)
|
std::vector<Font::DrawCommand> Font::generateVertices(const Codepoints &codepoints, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector offset, TextInfo *info)
|
||||||
{
|
{
|
||||||
// Spacing counter and newline handling.
|
// Spacing counter and newline handling.
|
||||||
float dx = offset.x;
|
float dx = offset.x;
|
||||||
@@ -360,87 +360,78 @@ std::vector<Font::DrawCommand> Font::generateVertices(const std::string &text, s
|
|||||||
|
|
||||||
// Pre-allocate space for the maximum possible number of vertices.
|
// Pre-allocate space for the maximum possible number of vertices.
|
||||||
size_t vertstartsize = vertices.size();
|
size_t vertstartsize = vertices.size();
|
||||||
vertices.reserve(vertstartsize + text.length() * 4);
|
vertices.reserve(vertstartsize + codepoints.size());
|
||||||
|
|
||||||
uint32 prevglyph = 0;
|
uint32 prevglyph = 0;
|
||||||
|
|
||||||
try
|
for (int i = 0; i < (int) codepoints.size(); i++)
|
||||||
{
|
{
|
||||||
utf8::iterator<std::string::const_iterator> i(text.begin(), text.begin(), text.end());
|
uint32 g = codepoints[i];
|
||||||
utf8::iterator<std::string::const_iterator> end(text.end(), text.begin(), text.end());
|
|
||||||
|
|
||||||
while (i != end)
|
if (g == '\n')
|
||||||
{
|
{
|
||||||
uint32 g = *i++;
|
if (dx > maxwidth)
|
||||||
|
maxwidth = (int) dx;
|
||||||
|
|
||||||
if (g == '\n')
|
// Wrap newline, but do not print it.
|
||||||
{
|
dy += floorf(getHeight() * getLineHeight() + 0.5f);
|
||||||
if (dx > maxwidth)
|
dx = offset.x;
|
||||||
maxwidth = (int) dx;
|
continue;
|
||||||
|
|
||||||
// Wrap newline, but do not print it.
|
|
||||||
dy += floorf(getHeight() * getLineHeight() + 0.5f);
|
|
||||||
dx = offset.x;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32 cacheid = textureCacheID;
|
|
||||||
|
|
||||||
const Glyph &glyph = findGlyph(g);
|
|
||||||
|
|
||||||
// If findGlyph invalidates the texture cache, re-start the loop.
|
|
||||||
if (cacheid != textureCacheID)
|
|
||||||
{
|
|
||||||
i = utf8::iterator<std::string::const_iterator>(text.begin(), text.begin(), text.end());
|
|
||||||
maxwidth = 0;
|
|
||||||
dx = offset.x;
|
|
||||||
dy = offset.y;
|
|
||||||
drawcommands.clear();
|
|
||||||
vertices.resize(vertstartsize);
|
|
||||||
prevglyph = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add kerning to the current horizontal offset.
|
|
||||||
dx += getKerning(prevglyph, g);
|
|
||||||
|
|
||||||
if (glyph.texture != 0)
|
|
||||||
{
|
|
||||||
// Copy the vertices and set their proper relative positions.
|
|
||||||
for (int j = 0; j < 4; j++)
|
|
||||||
{
|
|
||||||
vertices.push_back(glyph.vertices[j]);
|
|
||||||
vertices.back().x += dx;
|
|
||||||
vertices.back().y += dy + lineheight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if glyph texture has changed since the last iteration.
|
|
||||||
if (drawcommands.empty() || drawcommands.back().texture != glyph.texture)
|
|
||||||
{
|
|
||||||
// Add a new draw command if the texture has changed.
|
|
||||||
DrawCommand cmd;
|
|
||||||
cmd.startvertex = (int) vertices.size() - 4;
|
|
||||||
cmd.vertexcount = 0;
|
|
||||||
cmd.texture = glyph.texture;
|
|
||||||
drawcommands.push_back(cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawcommands.back().vertexcount += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance the x position for the next glyph.
|
|
||||||
dx += glyph.spacing;
|
|
||||||
|
|
||||||
// Account for extra spacing given to space characters.
|
|
||||||
if (g == ' ' && extra_spacing != 0.0f)
|
|
||||||
dx = floorf(dx + extra_spacing);
|
|
||||||
|
|
||||||
prevglyph = g;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (utf8::exception &e)
|
uint32 cacheid = textureCacheID;
|
||||||
{
|
|
||||||
throw love::Exception("UTF-8 decoding error: %s", e.what());
|
const Glyph &glyph = findGlyph(g);
|
||||||
|
|
||||||
|
// If findGlyph invalidates the texture cache, re-start the loop.
|
||||||
|
if (cacheid != textureCacheID)
|
||||||
|
{
|
||||||
|
i = 0;
|
||||||
|
maxwidth = 0;
|
||||||
|
dx = offset.x;
|
||||||
|
dy = offset.y;
|
||||||
|
drawcommands.clear();
|
||||||
|
vertices.resize(vertstartsize);
|
||||||
|
prevglyph = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add kerning to the current horizontal offset.
|
||||||
|
dx += getKerning(prevglyph, g);
|
||||||
|
|
||||||
|
if (glyph.texture != 0)
|
||||||
|
{
|
||||||
|
// Copy the vertices and set their proper relative positions.
|
||||||
|
for (int j = 0; j < 4; j++)
|
||||||
|
{
|
||||||
|
vertices.push_back(glyph.vertices[j]);
|
||||||
|
vertices.back().x += dx;
|
||||||
|
vertices.back().y += dy + lineheight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if glyph texture has changed since the last iteration.
|
||||||
|
if (drawcommands.empty() || drawcommands.back().texture != glyph.texture)
|
||||||
|
{
|
||||||
|
// Add a new draw command if the texture has changed.
|
||||||
|
DrawCommand cmd;
|
||||||
|
cmd.startvertex = (int) vertices.size() - 4;
|
||||||
|
cmd.vertexcount = 0;
|
||||||
|
cmd.texture = glyph.texture;
|
||||||
|
drawcommands.push_back(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawcommands.back().vertexcount += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance the x position for the next glyph.
|
||||||
|
dx += glyph.spacing;
|
||||||
|
|
||||||
|
// Account for extra spacing given to space characters.
|
||||||
|
if (g == ' ' && extra_spacing != 0.0f)
|
||||||
|
dx = floorf(dx + extra_spacing);
|
||||||
|
|
||||||
|
prevglyph = g;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort draw commands by texture first, and quad position in memory second
|
// Sort draw commands by texture first, and quad position in memory second
|
||||||
@@ -459,39 +450,57 @@ std::vector<Font::DrawCommand> Font::generateVertices(const std::string &text, s
|
|||||||
return drawcommands;
|
return drawcommands;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<Font::DrawCommand> Font::generateVertices(const std::string &text, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector offset, TextInfo *info)
|
||||||
|
{
|
||||||
|
Codepoints codepoints;
|
||||||
|
codepoints.reserve(text.size());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
utf8::iterator<std::string::const_iterator> i(text.begin(), text.begin(), text.end());
|
||||||
|
utf8::iterator<std::string::const_iterator> end(text.end(), text.begin(), text.end());
|
||||||
|
|
||||||
|
while (i != end)
|
||||||
|
{
|
||||||
|
uint32 g = *i++;
|
||||||
|
codepoints.push_back(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (utf8::exception &e)
|
||||||
|
{
|
||||||
|
throw love::Exception("UTF-8 decoding error: %s", e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateVertices(codepoints, vertices, extra_spacing, offset, info);
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const std::string &text, float wrap, AlignMode align, std::vector<GlyphVertex> &vertices, TextInfo *info)
|
std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const std::string &text, float wrap, AlignMode align, std::vector<GlyphVertex> &vertices, TextInfo *info)
|
||||||
{
|
{
|
||||||
if (wrap < 0.0f)
|
|
||||||
wrap = std::numeric_limits<float>::max();
|
wrap = std::max(wrap, 0.0f);
|
||||||
|
|
||||||
uint32 cacheid = textureCacheID;
|
uint32 cacheid = textureCacheID;
|
||||||
|
|
||||||
std::vector<DrawCommand> drawcommands;
|
std::vector<DrawCommand> drawcommands;
|
||||||
vertices.reserve(text.length() * 4);
|
vertices.reserve(text.length() * 4);
|
||||||
|
|
||||||
// wrappedlines indicates which lines were automatically wrapped. It
|
|
||||||
// has the same number of elements as lines_to_draw.
|
|
||||||
std::vector<bool> wrappedlines;
|
|
||||||
std::vector<int> widths;
|
std::vector<int> widths;
|
||||||
std::vector<std::string> lines;
|
std::vector<Codepoints> lines;
|
||||||
|
|
||||||
// We only need the list of wrapped lines in 'justify' mode.
|
getWrap(text, wrap, lines, &widths);
|
||||||
getWrap(text, wrap, lines, &widths, align == ALIGN_JUSTIFY ? &wrappedlines : nullptr);
|
|
||||||
|
|
||||||
float extraspacing = 0.0f;
|
|
||||||
int numspaces = 0;
|
|
||||||
int i = 0;
|
|
||||||
float y = 0.0f;
|
float y = 0.0f;
|
||||||
float maxwidth = 0;
|
float maxwidth = 0.0f;
|
||||||
|
|
||||||
for (const std::string &line : lines)
|
for (int i = 0; i < (int) lines.size(); i++)
|
||||||
{
|
{
|
||||||
extraspacing = 0.0f;
|
const auto &line = lines[i];
|
||||||
|
|
||||||
float width = (float) widths[i];
|
float width = (float) widths[i];
|
||||||
love::Vector offset(0.0f, floorf(y));
|
love::Vector offset(0.0f, floorf(y));
|
||||||
|
float extraspacing = 0.0f;
|
||||||
|
|
||||||
if (width > maxwidth)
|
maxwidth = std::max(width, maxwidth);
|
||||||
maxwidth = width;
|
|
||||||
|
|
||||||
switch (align)
|
switch (align)
|
||||||
{
|
{
|
||||||
@@ -502,12 +511,14 @@ std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const std::string
|
|||||||
offset.x = floorf((wrap - width) / 2.0f);
|
offset.x = floorf((wrap - width) / 2.0f);
|
||||||
break;
|
break;
|
||||||
case ALIGN_JUSTIFY:
|
case ALIGN_JUSTIFY:
|
||||||
numspaces = (int) std::count(line.begin(), line.end(), ' ');
|
{
|
||||||
if (wrappedlines[i] && numspaces >= 1)
|
float numspaces = (float) std::count(line.begin(), line.end(), ' ');
|
||||||
extraspacing = (wrap - width) / float(numspaces);
|
if (width < wrap && numspaces >= 1)
|
||||||
|
extraspacing = (wrap - width) / numspaces;
|
||||||
else
|
else
|
||||||
extraspacing = 0.0f;
|
extraspacing = 0.0f;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case ALIGN_LEFT:
|
case ALIGN_LEFT:
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -537,7 +548,6 @@ std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const std::string
|
|||||||
}
|
}
|
||||||
|
|
||||||
y += getHeight() * getLineHeight();
|
y += getHeight() * getLineHeight();
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info != nullptr)
|
if (info != nullptr)
|
||||||
@@ -636,15 +646,16 @@ int Font::getWidth(const std::string &str)
|
|||||||
while (getline(iss, line, '\n'))
|
while (getline(iss, line, '\n'))
|
||||||
{
|
{
|
||||||
int width = 0;
|
int width = 0;
|
||||||
|
uint32 prevglyph = 0;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
uint32 prevglyph = 0;
|
|
||||||
|
|
||||||
utf8::iterator<std::string::const_iterator> i(line.begin(), line.begin(), line.end());
|
utf8::iterator<std::string::const_iterator> i(line.begin(), line.begin(), line.end());
|
||||||
utf8::iterator<std::string::const_iterator> end(line.end(), line.begin(), line.end());
|
utf8::iterator<std::string::const_iterator> end(line.end(), line.begin(), line.end());
|
||||||
|
|
||||||
while (i != end)
|
while (i != end)
|
||||||
{
|
{
|
||||||
uint32 c = *i++;
|
uint32 c = *i++;
|
||||||
|
|
||||||
const Glyph &g = findGlyph(c);
|
const Glyph &g = findGlyph(c);
|
||||||
width += g.spacing + getKerning(prevglyph, c);
|
width += g.spacing + getKerning(prevglyph, c);
|
||||||
|
|
||||||
@@ -668,69 +679,117 @@ int Font::getWidth(char character)
|
|||||||
return g.spacing;
|
return g.spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Font::getWrap(const std::string &text, float wrap, std::vector<std::string> &lines, std::vector<int> *linewidths, std::vector<bool> *wrappedlines)
|
void Font::getWrap(const std::string &text, float wraplimit, std::vector<Codepoints> &lines, std::vector<int> *linewidths)
|
||||||
{
|
{
|
||||||
const float width_space = (float) getWidth(' ');
|
|
||||||
|
|
||||||
std::istringstream iss(text);
|
|
||||||
std::string line;
|
|
||||||
std::ostringstream string_builder;
|
|
||||||
|
|
||||||
// Split text at newlines.
|
// Split text at newlines.
|
||||||
|
std::string line;
|
||||||
|
std::istringstream iss(text);
|
||||||
|
|
||||||
|
// A wrapped line of text.
|
||||||
|
Codepoints wline;
|
||||||
|
|
||||||
while (std::getline(iss, line, '\n'))
|
while (std::getline(iss, line, '\n'))
|
||||||
{
|
{
|
||||||
std::vector<std::string> words;
|
|
||||||
std::istringstream word_iss(line);
|
|
||||||
|
|
||||||
// split line into words
|
|
||||||
std::copy(std::istream_iterator<std::string>(word_iss), std::istream_iterator<std::string>(), std::back_inserter(words));
|
|
||||||
|
|
||||||
float width = 0.0f;
|
float width = 0.0f;
|
||||||
float oldwidth = 0.0f;
|
uint32 prevglyph = 0;
|
||||||
string_builder.str("");
|
|
||||||
|
|
||||||
// Put words back together until a wrap occurs.
|
wline.clear();
|
||||||
for (const std::string &word : words)
|
wline.reserve(line.length());
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
float wordwidth = (float) getWidth(word);
|
utf8::iterator<std::string::const_iterator> i(line.begin(), line.begin(), line.end());
|
||||||
width += wordwidth;
|
utf8::iterator<std::string::const_iterator> end(line.end(), line.begin(), line.end());
|
||||||
|
|
||||||
// On wordwrap, push line to line buffer and clear string builder.
|
utf8::iterator<std::string::const_iterator> lastspaceit = end;
|
||||||
if (width > wrap && oldwidth > 0)
|
|
||||||
|
while (i != end)
|
||||||
{
|
{
|
||||||
int realw = (int) width;
|
uint32 c = *i;
|
||||||
|
|
||||||
// Remove trailing space.
|
const Glyph &g = findGlyph(c);
|
||||||
std::string tmp = string_builder.str();
|
float newwidth = width + g.spacing + getKerning(prevglyph, c);
|
||||||
lines.push_back(tmp.substr(0,tmp.size()-1));
|
|
||||||
string_builder.str("");
|
|
||||||
width = wordwidth;
|
|
||||||
realw -= (int) width;
|
|
||||||
|
|
||||||
if (linewidths)
|
// Wrap the line if it exceeds the wrap limit. Don't wrap yet if
|
||||||
linewidths->push_back(realw);
|
// we're processing a newline character, though.
|
||||||
|
if (c != ' ' && newwidth > wraplimit)
|
||||||
|
{
|
||||||
|
// If this is the first character in the line and it exceeds
|
||||||
|
// the limit, skip it completely.
|
||||||
|
if (wline.empty())
|
||||||
|
++i;
|
||||||
|
else if (lastspaceit != end)
|
||||||
|
{
|
||||||
|
// 'Rewind' to the last seen space, if the line has one.
|
||||||
|
// FIXME: This should preferably use vector::erase.
|
||||||
|
while (!wline.empty() && wline.back() != ' ')
|
||||||
|
wline.pop_back();
|
||||||
|
|
||||||
// Indicate that this line was automatically wrapped.
|
i = lastspaceit;
|
||||||
if (wrappedlines)
|
++i; // Start the next line after the space.
|
||||||
wrappedlines->push_back(true);
|
}
|
||||||
|
|
||||||
|
lines.push_back(wline);
|
||||||
|
|
||||||
|
if (linewidths)
|
||||||
|
linewidths->push_back(width);
|
||||||
|
|
||||||
|
prevglyph = 0; // Reset kerning information.
|
||||||
|
width = 0.0f;
|
||||||
|
wline.clear();
|
||||||
|
lastspaceit = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
width = newwidth;
|
||||||
|
prevglyph = c;
|
||||||
|
|
||||||
|
wline.push_back(c);
|
||||||
|
|
||||||
|
// Keep track of the last seen space, so we can "rewind" to it
|
||||||
|
// when wrapping.
|
||||||
|
if (c == ' ')
|
||||||
|
lastspaceit = i;
|
||||||
|
|
||||||
|
++i;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
string_builder << word << " ";
|
catch (utf8::exception &e)
|
||||||
|
{
|
||||||
width += width_space;
|
throw love::Exception("UTF-8 decoding error: %s", e.what());
|
||||||
oldwidth = width;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push last line.
|
// Remove trailing newlines.
|
||||||
|
if (!wline.empty() && wline.back() == '\n')
|
||||||
|
wline.pop_back();
|
||||||
|
|
||||||
|
lines.push_back(wline);
|
||||||
|
|
||||||
if (linewidths)
|
if (linewidths)
|
||||||
linewidths->push_back(width);
|
linewidths->push_back(width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::string tmp = string_builder.str();
|
void Font::getWrap(const std::string &text, float wraplimit, std::vector<std::string> &lines, std::vector<int> *linewidths)
|
||||||
lines.push_back(tmp.substr(0,tmp.size()-1));
|
{
|
||||||
|
std::vector<Codepoints> codepointlines;
|
||||||
|
getWrap(text, wraplimit, codepointlines, linewidths);
|
||||||
|
|
||||||
// Indicate that this line was not automatically wrapped.
|
std::string line;
|
||||||
if (wrappedlines)
|
|
||||||
wrappedlines->push_back(false);
|
for (const Codepoints &codepoints : codepointlines)
|
||||||
|
{
|
||||||
|
line.clear();
|
||||||
|
line.reserve(codepoints.size());
|
||||||
|
|
||||||
|
for (uint32 codepoint : codepoints)
|
||||||
|
{
|
||||||
|
char character[5] = {'\0'};
|
||||||
|
char *end = utf8::unchecked::append(codepoint, character);
|
||||||
|
line.append(character, end - character);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push_back(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ class Font : public Object, public Volatile
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
||||||
|
typedef std::vector<uint32> Codepoints;
|
||||||
|
|
||||||
enum AlignMode
|
enum AlignMode
|
||||||
{
|
{
|
||||||
ALIGN_LEFT,
|
ALIGN_LEFT,
|
||||||
@@ -93,7 +95,9 @@ public:
|
|||||||
|
|
||||||
virtual ~Font();
|
virtual ~Font();
|
||||||
|
|
||||||
|
std::vector<DrawCommand> generateVertices(const Codepoints &str, std::vector<GlyphVertex> &vertices, float extra_spacing = 0.0f, Vector offset = {}, TextInfo *info = nullptr);
|
||||||
std::vector<DrawCommand> generateVertices(const std::string &text, std::vector<GlyphVertex> &vertices, float extra_spacing = 0.0f, Vector offset = Vector(), TextInfo *info = nullptr);
|
std::vector<DrawCommand> generateVertices(const std::string &text, std::vector<GlyphVertex> &vertices, float extra_spacing = 0.0f, Vector offset = Vector(), TextInfo *info = nullptr);
|
||||||
|
|
||||||
std::vector<DrawCommand> generateVerticesFormatted(const std::string &text, float wrap, AlignMode align, std::vector<GlyphVertex> &vertices, TextInfo *info = nullptr);
|
std::vector<DrawCommand> generateVerticesFormatted(const std::string &text, float wrap, AlignMode align, std::vector<GlyphVertex> &vertices, TextInfo *info = nullptr);
|
||||||
|
|
||||||
void drawVertices(const std::vector<DrawCommand> &drawcommands);
|
void drawVertices(const std::vector<DrawCommand> &drawcommands);
|
||||||
@@ -140,13 +144,12 @@ public:
|
|||||||
* and optionally the number of lines
|
* and optionally the number of lines
|
||||||
*
|
*
|
||||||
* @param text The input text
|
* @param text The input text
|
||||||
* @param wrap The number of pixels to wrap at
|
* @param wraplimit The number of pixels to wrap at
|
||||||
* @param max_width Optional output of the maximum width
|
* @param max_width Optional output of the maximum width
|
||||||
* @param wrapped_lines Optional output indicating which lines were
|
|
||||||
* auto-wrapped. Indices correspond to indices of the returned value.
|
|
||||||
* Returns a vector with the lines.
|
* Returns a vector with the lines.
|
||||||
**/
|
**/
|
||||||
void getWrap(const std::string &text, float wrap, std::vector<std::string> &lines, std::vector<int> *line_widths = 0, std::vector<bool> *wrapped_lines = 0);
|
void getWrap(const std::string &text, float wraplimit, std::vector<std::string> &lines, std::vector<int> *line_widths = nullptr);
|
||||||
|
void getWrap(const std::string &text, float wraplimit, std::vector<Codepoints> &lines, std::vector<int> *line_widths = nullptr);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the line height (which should be a number to multiply the font size by,
|
* Sets the line height (which should be a number to multiply the font size by,
|
||||||
|
|||||||
Reference in New Issue
Block a user