Jump to content
  • Advertisement
Sign in to follow this  

Trying to add support for newline and word wrap

This topic is 3084 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hello. I've got a question that I can't find the right place to ask. I hope it's allright to ask here. I've been using a tutorial/source to create font and text support in my OpenGL application. Now I'm stuck again with this, trying to add new line and wrap support. I just don't know where to start, I guess. I think I've got a good grasp of the source code but I don't really know how to progress from here. I know that I have to figure out the width of the "text area" to get wrapping to work and I know that I need the height of the font to get '\n' to take, but that's about as far as I've come, figuring this out. I'm making posts all over this forum trying to understand and learn. A year ago I began learning C++ and OpenGL and so far I'm pretty satisfied with what I've learned. I'm still very new though :) source code:
// freetype.h
#ifndef FREE_TYPE_H
#define FREE_TYPE_H

#pragma warning(disable: 4786)

#include <ft2build.h>
#include <freetype/freetype.h>
#include <freetype/ftglyph.h>
#include <freetype/ftoutln.h>
#include <freetype/fttrigon.h>

#include <windows.h>
#include <gl/gl.h>
#include <gl/glu.h>

#include <vector>
#include <string>



namespace freetype{
	using std::vector;
	using std::string;

	struct char_data {
		int w, h;
		int advance;
		int left;
		int move_up;
		unsigned char *data;

		char_data(char ch, FT_Face face){
			if(FT_Load_Glyph(face, FT_Get_Char_Index(face, ch), FT_LOAD_DEFAULT)){
				// error
			}

			FT_Glyph glyph;
			if(FT_Get_Glyph(face->glyph, &glyph)){
				// error
			}

			FT_Glyph_To_Bitmap(&glyph, ft_render_mode_normal, 0, 1);
			FT_BitmapGlyph bitmap_glyph = (FT_BitmapGlyph)glyph;

			FT_Bitmap& bitmap = bitmap_glyph->bitmap;

			advance = face->glyph->advance.x >> 6;
			left = bitmap_glyph->left;
			w = bitmap.width;
			h = bitmap.rows;
			move_up = bitmap_glyph->top - bitmap.rows;

			data = new unsigned char[2 * w * h];
			for(int x = 0; x < w; x++){
				for(int y = 0; y < h; y++){
					const int my = h - 1 - y;
					data[2 * (x + w * my)] = 255;
					data[2 * (x + w * my) + 1] = bitmap.buffer[x + w * y];
				}
			}
		}

		~char_data() { delete [] data; }
	};

	struct font_data {
		char_data *chars[128];
		float h;

		void init(const char *fname, unsigned int h);

		void clean();
	};

	void print(const font_data &ft_font, const char *fmt, ...);
}

#endif

// freetype.cpp
#include <sstream>

#include "freetype.h"

using namespace std;

// snipped out the "write" function referenced in the namespace
// the write function writes messages to my console window

namespace freetype {
	void font_data::init(const char *fname, unsigned int h){
		this->h = h;

		FT_Library library;
		if(FT_Init_FreeType(&library)){
			write(cH, "FT_Init_FreeType failed!");
		}

		FT_Face face;
		if(FT_New_Face(library, fname, 0, &face)){
			write(cH, "FT_New_Face failed! (problem with the font file)");
		}

		FT_Set_Char_Size(face, h << 6, h << 6, 96, 96);
		
		for(int i = 0; i < 128; i++){
			chars = new char_data(i, face);
		}

		FT_Done_Face(face);
		FT_Done_FreeType(library);
	}

	void font_data::clean(){
		for(int i = 0; i < 128; i++){
			delete chars;
		}
	}

	inline void move_raster_x(int x){
		glBitmap(0, 0, 0, 0, x, 0, NULL);
	}
	inline void move_raster_y(int y){
		glBitmap(0, 0, 0, 0, 0, y, NULL);
	}

	void print(const font_data &ft_font, const char *fmt, ...){
		char text[256];
		va_list ap;

		if(fmt == NULL){
			*text = 0;
		}else{
			va_start(ap, fmt);
				vsprintf(text, fmt, ap);
			va_end(ap);
		}

		glPushAttrib(GL_CURRENT_BIT | GL_PIXEL_MODE_BIT | GL_ENABLE_BIT);
			glDisable(GL_TEXTURE_2D);
			glDisable(GL_DEPTH_TEST);
			glEnable(GL_BLEND);
			glDisable(GL_LIGHTING);
			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

			GLint old_unpack;
			glGetIntegerv(GL_UNPACK_ALIGNMENT, &old_unpack);
			glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

			float color[4];
			glGetFloatv(GL_CURRENT_COLOR, color);

			glPixelTransferf(GL_RED_SCALE, color[0]);
			glPixelTransferf(GL_GREEN_SCALE, color[1]);
			glPixelTransferf(GL_BLUE_SCALE, color[2]);
			glPixelTransferf(GL_ALPHA_SCALE, color[3]);

			for(int i = 0; text; i++){
				const char_data &cdata = *ft_font.chars[text];

				move_raster_x(cdata.left);
				move_raster_y(cdata.move_up);

					glDrawPixels(cdata.w, cdata.h, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, cdata.data);

				move_raster_y(-cdata.move_up);
				move_raster_x(cdata.advance - cdata.left);
			}

			glPixelStorei(GL_UNPACK_ALIGNMENT, old_unpack);
		glPopAttrib();
	}
}

// main.cpp
#include "freetype.h"

// snipped

freetype::font_data normal_text;

// snipped

bool initialize(GL_Window *window, Keys *keys, Mouse *mouse){
	// snipped out
	normal_text.init("fonts/Dustismo.ttf", 9);
	return true;
}

void drawText(void){
	glPushMatrix();
		glColor3f(1, 1, 1);
		glRasterPos2f(5, .5);
		freetype::print(normal_text, "test text\ntest\ntest\ntest");
	glPopMatrix();
}

void draw(void){
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	glTranslatef(0.0f, 0.0f, -20.0f);


	RotateScene(EYE_RADIUS, RotateCamY, RotateCamX, 180.0);
	lighting();


	glPushMatrix();
		drawGlobe(4.0f, 100);
	glPopMatrix();

	glPushMatrix();
		drawStars(200.0f, 10);
	glPopMatrix();


	glPushMatrix();
		glLoadIdentity();
			glTranslatef(0.0f, 0.0f, -20.0f);
			drawText();
	glPopMatrix();


	glFlush();
}

I can provide any snipped code if you need to see it. I hope someone might be able to help me with this. The theory I've got is that to get \n to work I need to get the height and if I hit a \n in the text string I need to push the text down the height of the text. And to get wrapping to work I need to calculate the length of the text and compare it with some sort of container. When the length is larger than that container I have to make a new line automatically. But having the theory is only one step of the way. I can't for the life of me understand how to implement it correctly. Thanks in advance, Marcus Axelsson [Edited by - tre on December 9, 2009 2:44:56 AM]

Share this post


Link to post
Share on other sites
Advertisement
here's some stuff that might help you go in the right direction at least...

#1 - in the function that draws text, give it a rectangle for the valid area for it to render text into. A rectangle gives you the location to start rendering (IE upper left corner of the rectangle) and also gives you a width and a height. This will help you figure out where to word wrap, and will also let you calculate offsets when you hit a \n etc. If you want to get real fancy later on, you can also do a "scisor test" so that you can scroll the text in the box and it'll look like how windows renders scrolled text.

#2 - to render the text have a loop something like (psuedo code):



CurrentLine = 0; //what line are we on currently
StartIndex = 0; //the index of the first character of our current line
StopIndex = 0; //the index of the last character of our current line
CurrentLineWidth = 0; //

while (StartIndex < StringLength)
{
StartIndex = StopIndex;

while (StopIndex < StringLength && CurrentLineWidth < PrintWidth && String[StopIndex] != '\n')
{
CurrentLineWidth += WidthOfCharacter(String[StopIndex]);
StopIndex++;
}

PrintSubString(CurrentLine,StartIndex,StopIndex);

if(String[StopIndex]=='\n')
StopIndex++;

CurrentLine++;
}




That's really rough and untested but hopefully gives you an idea.

The above code also doesn't break a line at whitespace unfortunately but if you get it working like that first then afterwords you can work on getting it to break at a whitespace (:

one way would be as you scan through the current line, keep an index of where the last white space character was so that when you go over length, you just set the stop index to be the position of the last white space character.

hope this helps!

Share this post


Link to post
Share on other sites
I have done this too, though I wasn't satisfied. I also want to add anchors / clipping, text selection, multiple fonts in one line etc.

But for basic wrap and new line, that's doable:

You need to know the width of each character. Traverse the given string, and count the width's of the characters you had. If the next characters width makes it go beyond the constraint, go to the next line (aka. set x offset to zero, and add line_height to y offset).

If you encounter \n, \r or \r\n also set x offset to zero, and add line_height to y offset.

Now, if you want word wrap, as opposed to character wrap, remember the x position of the last space. When the next word goes beyond the constrained, wrap at the space.

Now, to make it fit the line, count the width's of the words, subtract this from the maximal width of the line, divide by the number of space and there's the width spaces should have. Unless no spaces are there, you need to pad the characters.

It hasn't got much to do with C++ of OpenGL, it's more to do with basic math really. Think it out well, the way you want to tackle it.

Here is a bit of my code. Cannot guarantee it works (but I think it does), since I work on a new method. But it does give you a global idea of how it can be done.

        void RichText::calculate() const
{
if (!_font)
{
WARNING("no font set");
}

rows.clear();

// minimal line height
unsigned int line_height = 0;
if (_line_height)
{
line_height += _line_height;
}
else
{
line_height += _font->height();
}

// lets stay off the original
std::string tmp_text(_text + '\n');

size_t i = 0;
float y = _font->ascender();
size_t tmp_found = 0, found = tmp_text.find_first_of('\n');
while (found != std::string::npos)
{
if (_bounding_box.h && (y + _font->descender()) > _bounding_box.h)
{
break;
}

// prepare for calculating row
Row row;
std::string tmp_row = tmp_text.substr(0, found) + ' ';

if (tmp_row.length() == 1)
{
rows.push_back(Row());
tmp_found = 0;
}
else
{
// should it change space / char width?
bool change_width = false;
if (_horizontal_justification == Justification::Justify || _horizontal_justification == Justification::JustifyAll)
{
change_width = true;

// if the row ends earlier
if (_horizontal_justification == Justification::Justify && _bounding_box.w && (int) _font->width(tmp_row) < _bounding_box.w)
{
change_width = false;
}
}

// calc the row
row.space_width = _font->width(' ');
tmp_found = calculate_line(tmp_row, row.row_width, row.space_width, row.char_width, change_width);

if (_bounding_box.w)
{
if (_horizontal_alignment == Alignment::Middle)
{
row.offset = (_bounding_box.w - row.row_width) / 2.0f;
}
else if (_horizontal_alignment == Alignment::Right)
{
row.offset = (_bounding_box.w - row.row_width);
}
}

if (tmp_found)
{
row.text = tmp_row.substr(0, tmp_found - 1); // found - 1 = without the found space
}

// store it
rows.push_back(row);
}
++i;

// when the next character is a space or new line, move up 1 position (lines never start or end with a space)
// if the following is true, it means calculate_line had to character wrap in a word
if (tmp_found && tmp_text.at(tmp_found - 1) != ' ' && tmp_text.at(tmp_found - 1) != '\n')
{
tmp_found -= 1;
}

// always advance at least one! to prevent endless loops or double iterations
if (tmp_found < 1)
{
tmp_found = 1;
}

y += line_height;

// next!
tmp_text = tmp_text.substr(tmp_found);
found = tmp_text.find_first_of('\n');
}

if (found == std::string::npos)
{
--i; // whenever we reach found == std::string::npos
}

section.offset = 0.0f;
section.row_height = 0.0f;

// vertical expansion
if (_bounding_box.h)
{
if (_vertical_justification == Justification::Justify && i && (i * line_height) < (unsigned int) _bounding_box.h)
{
section.row_height = (_bounding_box.h - (i * line_height)) / (i - 1); // i - 1 = number of spaces between lines
}

// if it's not taller then the box
if (((i * (line_height + section.row_height)) - section.row_height) < _bounding_box.h)
{
if (_vertical_alignment == Alignment::Middle)
{
section.offset = (_bounding_box.h - ((i * (line_height + section.row_height)) - section.row_height)) / 2.0f;
}
else if (_vertical_alignment == Alignment::Bottom)
{
section.offset = (_bounding_box.h - ((i * (line_height + section.row_height)) - section.row_height));
}
}
}
changed_text = false;
changed_view = true;
}

size_t RichText::calculate_line(std::string row, float &row_width, float &space_width, float &char_width, bool change_width) const
{
size_t i = 0;
size_t spaces = 0;
size_t words = 0;

float x = 0.0f, advance = 0.0f;
size_t tmp_found = 0, found = row.find_first_of(' ');
while (found != std::string::npos)
{
// word length
advance = _font->width(row.substr(0, found));

// word wrap
if (_wrap == Wrap::WordWrap && words && _bounding_box.w && (x + advance) > _bounding_box.w)
{
--i;
x -= space_width;
--spaces;
break;
}

// draw the word
tmp_found = calculate_word(row.substr(0, found), x);
i += tmp_found;
x += advance; // whole word added

// char wrap
if ((_wrap == Wrap::CharacterWrap || (_wrap == Wrap::WordWrap && !words)) && _bounding_box.w && x > _bounding_box.w)
{
x -= advance; // no, not the whole word added

// add the actual ammount of the word that will be displayed
if (tmp_found)
{
x += _font->width(row.substr(0, tmp_found));

if (row.at(tmp_found) == ' ')
{
++i; // add 1 to the length, since the calling function cuts one off
}
}
// if we didn't process any characters, the last character on the row would be a space
// or the first character of the word didn't fit!
else
{
if (row.at(tmp_found) == ' ')
{
--i;
x -= space_width;
--spaces;
}
else
{
return 0;
}
}
break;
}
++words;

// add the space
++tmp_found;
++i;
x += space_width;
++spaces;

// and do it again
row = row.substr(tmp_found);
found = row.find_first_of(' ');
}

// don't count the added space
if (found == std::string::npos)
{
--i;
x -= space_width;
--spaces;
}

row_width = x;
if (_bounding_box.w && change_width)
{
// increase space width
if (spaces)// && ((_bounding_box.w - (x - (spaces * space_width))) / spaces) <= (space_width * 10))
{
x -= (spaces * space_width);
space_width = (_bounding_box.w - x) / spaces;
row_width = x + (spaces * space_width);
}
// increase char width
else
{
--i; // number of characters - 1 = number of spaces between characters
if (i)
{
char_width = ((_bounding_box.w - x) / i);
row_width = x + (i * char_width);
}
++i; // back to the number of characters
}
}
++i;
return i;
}

size_t RichText::calculate_word(const std::string &word, float x) const
{
// track how far it came till the line ended (for char wrap)
size_t i = 0;

float advance = 0.0f;
for (; i < word.length(); ++i)
{
// char length
if (_font->kerning() && (i + 1) < word.length())
{
advance = _font->kerning((char) *word.substr(i, 1).c_str(), (char) *word.substr(i + 1, 1).c_str());
}
else
{
advance = _font->width((char) *word.substr(i, 1).c_str());
}

// char wrap
if (_bounding_box.w && (x + advance) > _bounding_box.w)
{
break;
}

// move it up
x += advance;
}
return i;
}

Share this post


Link to post
Share on other sites
Thanks both of you for helping me out.
I've decided to tackle newlines first and then going with a bounding box, trying to figure out the length of the text and so on.

The newline code then. I'm having problems as I thought I would.
I can make new lines with my code but I can't figure out how to get the text to the left.
I got it working with ONE newline, but adding another pushed that line way to the left and the next one after that more to the left until the text disappeared.

This is what I have so far:
// relevant information from "freetype.h"
advance = face->glyph->advance.x >> 6;
left = bitmap_glyph->left;
w = bitmap.width;
h = bitmap.rows;
move_up = bitmap_glyph->top - bitmap.rows;



// snippet from "freetype.cpp"
for(int i = 0; text; i++){
const char_data &cdata = *ft_font.chars[text];

move_raster_x(cdata.left);
move_raster_y(cdata.move_up);

glDrawPixels(cdata.w, cdata.h, GL_LUMINANCE_ALPHA,
GL_UNSIGNED_BYTE, cdata.data);

if(text == '\n'){
move_raster_y((cdata.move_up - cdata.h) - 4);
move_raster_x(((-cdata.advance - cdata.left) - 1) * i);
}else{
move_raster_y(-cdata.move_up);
move_raster_x((cdata.advance - cdata.left) + 1);
}
}

Problems with this code is that I can't for the life of me remove the strange square icon that appears instead of \n in the text and I just can't figure out how to get the text to the left for each new line.

If you need more code, as allways: just ask :)
I think this stems from me not having a better grasp of what this particular code does. I've been reading up on the FT API reference but it's moving slowly.
If you've got any solutions, please let me know.

If I reach a conclusion I will be posting here. Hopefully I'll be able to get both newline and word wrapping working in the near future :)

Share this post


Link to post
Share on other sites
May I ask why you use bitmaps? It makes it unnecessarily difficult and is even (much) slower.

// snippet from "freetype.cpp"
move_raster_x(cdata.left);
move_raster_y(cdata.move_up);

for (int i = 0; text; i++)
{
const char_data &cdata = *ft_font.chars[text];

glDrawPixels(cdata.w, cdata.h, GL_LUMINANCE_ALPHA,
GL_UNSIGNED_BYTE, cdata.data);

if (text == '\n')
{
move_raster_y(-cdata.h - 4); // or cdata.h + 4 depends how y is orientated
move_raster_x(-(cdata.advance + 1) * i);
}
else
{
move_raster_x(cdata.advance + 1);
}
}



You do a lot of moving of the position which is not needed, you _only_ need to change y when making a new line...you even touched it for new characters. Also you constantly added to later on subtract it, not very efficient.

What I did for my font system, is generating a texture from the font file, then by drawing quads you render parts of the texture. Then you keep track of x and y coordinates as floats and pass those for quad drawing (most efficiently in a VBO but that might be slightly advanced at this point).

Share this post


Link to post
Share on other sites
Quote:
Original post by Decrius
May I ask why you use bitmaps? It makes it unnecessarily difficult and is even (much) slower.

*** Source Snippet Removed ***

You do a lot of moving of the position which is not needed, you _only_ need to change y when making a new line...you even touched it for new characters. Also you constantly added to later on subtract it, not very efficient.

What I did for my font system, is generating a texture from the font file, then by drawing quads you render parts of the texture. Then you keep track of x and y coordinates as floats and pass those for quad drawing (most efficiently in a VBO but that might be slightly advanced at this point).


I have done what you did in your code snippet. This results in excellent new lines but the text gets pushed to the left quite a bit for every \n, which results in (right now) that the first new line is a bit too much to the left, maybe 5-6 pixels, the second new line is pushed about three times as much as that and the third new line is practically in the center of the screen (the text begins a bit to the right.
I can't get the text to align to the left.
Edit:
I also want to explain why I'm using two sets of move_raster_x/y. If I don't do it, say for the word "jalapeño", it would push the J and the P up to align on the bottom with the rest of the word, which is not good. If I only do it once the text gets pushed down each time a letter comes up that has it's ending below the "line". So to speak. The X move helps with keeping the letters in line. If I don't use it some of the letters gets pushed to the left.

About using bitmaps.
The source used bitmaps and I needed to get away from quads because I couldn't set colour when I used quads - it stayed black whatever I tried. I made a thread about that earlier but couldn't get a solution.
I found this code which allows me to change colour to whatever I want, very quickly. It might not be the perfect solution but at least I can work with it now.
I have been using the NeHe tutorial 43 to display TrueType text in OpenGL. So that's the tutorial if you want to check it out: Lesson 43

Share this post


Link to post
Share on other sites
Just a question (I didn't look through the codes). Do you skip the newline character after you break the line?
If not, it will be drawn, but it's a white character, which probably means that a space will be drawn. So the new line will seem to be offsetting.

Share this post


Link to post
Share on other sites
Quote:
Original post by szecs
Just a question (I didn't look through the codes). Do you skip the newline character after you break the line?
If not, it will be drawn, but it's a white character, which probably means that a space will be drawn. So the new line will seem to be offsetting.


I did not skip the newline, true.
Now I do, though :)
And it gives a bit better newlines, but only for 3 newlines in a row. Each of these get pushed a little bit to the left. Going to newline 4 it gets pushed to the left a lot, 5 goes into the middle and 6 is allmost all the way to the left.

Not only that. Newline 4, 5 and 6 doesn't get pushed down as much as line 1, 2, 3. So line 4 is connecting with line 3.

Confused.

Edit:
Here's how it looks:
text error 1

And this is the current code:
bool isNewline = false;

for(int i = 0; text; i++){
const char_data &cdata = *ft_font.chars[text];

move_raster_x(cdata.left);
move_raster_y(cdata.move_up);

if(text[i+1] == '\n'){
i += 1;
isNewline = true;
}

glDrawPixels(cdata.w, cdata.h, GL_LUMINANCE_ALPHA,
GL_UNSIGNED_BYTE, cdata.data);

if(isNewline){
move_raster_y(-cdata.h - 5);
move_raster_x(-(cdata.advance + 1) * i);
isNewline = false;
}else{
move_raster_y(-cdata.move_up);
move_raster_x(cdata.advance + 1);
}
}

Share this post


Link to post
Share on other sites
move_raster_x requires relative values.

Are you sure the freetype is proportional? (All letters have the same width?)

Set glRasterPos every newlines instead, so you always place it the same x.

Share this post


Link to post
Share on other sites
Not related to the issue, but:
you do this:

if(text[i+1] == '\n')

What happens if the first character is a newline?

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

Participate in the game development conversation and more when you create an account on GameDev.net!

Sign me up!