#include #include #include #include #include #ifdef TTF #ifndef WINDOWS #include #endif #include #endif // TTF #include "font.h" #include "asc.h" #include "chat.h" #include "colors.h" #include "elconfig.h" #include "elloggingwrapper.h" #include "exceptions/extendedexception.hpp" #include "io/elpathwrapper.h" #include "init.h" #include "gl_init.h" #include "textures.h" namespace { #ifdef TTF int next_power_of_two(int n) { int res = 1; while (res < n) res *= 2; return res; } #endif size_t memcspn(const unsigned char* text, size_t len, const unsigned char* reject, size_t len_reject) { size_t i; bool rej[256] = { false }; for (i = 0; i < len_reject; ++i) rej[reject[i]] = true; for (i = 0; i < len && !rej[text[i]]; ++i) /* do nothing */; return i; } bool filter_messages(const text_message *msgs, size_t msgs_size, Uint8 filter, size_t msg_start, size_t &imsg, size_t &ichar) { #ifndef MAP_EDITOR2 if (filter != FILTER_ALL) { // skip all messages of the wrong channel while (skip_message(&msgs[imsg], filter)) { if (++imsg >= msgs_size) imsg = 0; ichar = 0; if (imsg == msg_start || !msgs[imsg].data || imsg == msg_start) return true; } } #endif return !msgs[imsg].data || msgs[imsg].deleted; } bool pos_selected(const select_info *sel, size_t imsg, size_t ichar) { if (!sel || TEXT_FIELD_SELECTION_EMPTY(sel)) return false; std::pair start = std::make_pair(size_t(sel->sm), size_t(sel->sc)); std::pair end = std::make_pair(size_t(sel->em), size_t(sel->ec)); if (end < start) std::swap(start, end); return (imsg > start.first || (imsg == start.first && ichar >= start.second)) && (imsg < end.first || (imsg == end.first && ichar <= end.second)); } #ifdef TTF TTF_Font* open_font(const std::string& file_name, int point_size) { // First try to interpret ttf_file_name as a path relative to the data directory. If that fails, // try a path relative to the ttf_directory. If that also fails, try as an absolute path. std::string path = datadir + file_name; TTF_Font *font = TTF_OpenFont(path.c_str(), point_size); if (!font) { path = ttf_directory + file_name; font = TTF_OpenFont(path.c_str(), point_size); if (!font) { font = TTF_OpenFont(file_name.c_str(), point_size); } } return font; } #endif } // namespace namespace eternal_lands { FontOption::FontOption(size_t font_nr): _font_nr(font_nr), _file_name(), _file_base_name(), _font_name(), #ifdef TTF _is_ttf(false), #endif _fixed_width(), _failed(false) { static const std::array file_names = { { "textures/font.dds", "textures/font.dds", "textures/font2.dds", "textures/font3.dds", "textures/font5.dds", "textures/font6.dds", "textures/font7.dds" } }; std::ostringstream os; if (font_nr > file_names.size()) { _failed = true; LOG_ERROR("Invalid font number %zu", font_nr); } else if (font_nr == 0) { _file_name = file_names[font_nr]; _font_name = "Type 1 (fixed)"; } else { _file_name = file_names[font_nr]; size_t begin = _file_name.find_last_of('/') + 1; size_t end = _file_name.find_last_of('.'); os << "Type " << font_nr << " - " << _file_name.substr(begin, end); _font_name = os.str(); } if (!el_file_exists(_file_name.c_str())) { LOG_ERROR("Unable to find font file '%s'", _file_name.c_str()); _failed = true; } size_t sep_pos = _file_name.find_last_of("/\\"); _file_base_name = sep_pos == std::string::npos ? _file_name : _file_name.substr(sep_pos + 1); _fixed_width = (font_nr != 1 && font_nr != 2); } #ifdef TTF FontOption::FontOption(const std::string& file_name): _font_nr(std::numeric_limits::max()), _file_name(file_name), _file_base_name(), _font_name(), _is_ttf(true), _fixed_width(), _failed(false) { TTF_Font *font = open_font(file_name.c_str(), 40); if (!font) { LOG_ERROR("Failed to open TTF font file '%s'", file_name.c_str()); _failed = true; return; } // Quick check to see if the font is useful if (!TTF_GlyphIsProvided(font, 'A') || !TTF_GlyphIsProvided(font, '!')) { // Nope, can't render in this font TTF_CloseFont(font); LOG_ERROR("Unable to render text with TTF font file '%s'", file_name.c_str()); _failed = true; return; } size_t sep_pos = _file_name.find_last_of("/\\"); _file_base_name = sep_pos == std::string::npos ? _file_name : _file_name.substr(sep_pos + 1); std::string name = TTF_FontFaceFamilyName(font); std::string style = TTF_FontFaceStyleName(font); _font_name = name + ' ' + style; _fixed_width = (TTF_FontFaceIsFixedWidth(font) != 0); TTF_CloseFont(font); } #endif // TTF void FontOption::add_select_options(bool add_button) const { add_multi_option_with_id("ui_font", _font_name.c_str(), _file_base_name.c_str(), add_button); add_multi_option_with_id("chat_font", _font_name.c_str(), _file_base_name.c_str(), add_button); add_multi_option_with_id("name_font", _font_name.c_str(), _file_base_name.c_str(), add_button); add_multi_option_with_id("book_font", _font_name.c_str(), _file_base_name.c_str(), add_button); add_multi_option_with_id("note_font", _font_name.c_str(), _file_base_name.c_str(), add_button); add_multi_option_with_id("rules_font", _font_name.c_str(), _file_base_name.c_str(), add_button); if (is_fixed_width()) { add_multi_option_with_id("encyclopedia_font", _font_name.c_str(), _file_base_name.c_str(), add_button); } } const std::array TextDrawOptions::default_foreground_color = { 1.0f, 1.0f, 1.0f }; const std::array TextDrawOptions::default_background_color = { 0.0f, 0.0f, 0.0f }; const std::array TextDrawOptions::default_selection_color = { 1.0f, 0.635f, 0.0f }; TextDrawOptions::TextDrawOptions(): _max_width(0), _max_lines(0), _zoom(1.0), _line_spacing(1.0), _alignment(LEFT), _vertical_alignment(TOP_LINE), _flags(0), _fg_color{{-1.0, -1.0, -1.0}}, _bg_color{{-1.0, -1.0, -1.0}}, _sel_color(default_selection_color) {} void TextDrawOptions::use_background_color() const { if (has_background_color()) glColor3fv(_bg_color.data()); else glColor3fv(default_background_color.data()); } void TextDrawOptions::use_foreground_color() const { if (has_foreground_color()) glColor3fv(_fg_color.data()); else glColor3fv(default_foreground_color.data()); } void TextDrawOptions::use_selection_color() const { glColor3fv(_sel_color.data()); } // Relative letter frequencies for English text, based on the counts in // // M.N. Jones and D.J.K. Mewhort, // Behavior Research Methods, Instruments, & Computers 36, pp. 388-396 (2004) // // These are used to compute an "average" character width for a font. When // using this, please note that: // 1) This is based on English text, so these frequencies may not be applicable // to text in to other languages, // 2) It is based on text from the New York Times, and so may not be // representative for text used in EL, // 3) Only ASCII characters were counted (so accented characters don't contribute), // and a few ASCII characters are missing as well ('[', '\', ']', '^', '_', // and '`'). // 4) The occurance of the space character was guesstimated from the reported total // number of words in the corpus. // // When you need to be sure a text will fit in a certain space, use the maximum // character width or use a monospaced font. const std::array Font::letter_freqs = { 1647, 0, 33, 0, 6, 0, 1, 24, 6, 6, 2, 0, 116, 30, 111, 1, 64, 54, 39, 22, 23, 44, 18, 14, 21, 33, 6, 4, 0, 0, 0, 1, 0, 33, 20, 27, 15, 16, 12, 11, 15, 26, 9, 5, 13, 31, 24, 12, 17, 1, 17, 36, 38, 7, 4, 13, 1, 11, 1, 0, 0, 0, 0, 0, 0, 619, 102, 231, 279, 911, 153, 142, 348, 533, 8, 54, 300, 173, 534, 556, 148, 6, 487, 492, 648, 190, 77, 119, 15, 125, 8, 0, 0, 0, 0 }; const ustring Font::ellipsis = reinterpret_cast("..."); Font::Font(Font&& font): _font_name(font.font_name()), _file_name(font.file_name()), _flags(font._flags), _texture_width(font._texture_width), _texture_height(font._texture_height), _metrics(font._metrics), _block_width(font._block_width), _line_height(font._line_height), _vertical_advance(font._vertical_advance), _font_top_offset(font._font_top_offset), _digit_center_offset(font._digit_center_offset), _password_center_offset(font._password_center_offset), _max_advance(font._max_advance), _max_digit_advance(font._max_digit_advance), _max_name_advance(font._max_name_advance), _avg_advance(font._avg_advance), _spacing(font._spacing), _scale_x(font._scale_x), _scale_y(font._scale_y), #ifdef TTF _point_size(font._point_size), _outline(font._outline), #endif _texture_id(font._texture_id) { font._flags &= ~HAS_TEXTURE; } Font::Font(const FontOption& option): _font_name(option.font_name()), _file_name(option.file_name()), _flags(0), _texture_width(256), _texture_height(256), _metrics(), _block_width(font_block_width), _line_height(default_vertical_advance + 1), _vertical_advance(default_vertical_advance), _font_top_offset(0), _digit_center_offset(option.font_number() == 2 ? 10 : 9), _password_center_offset(0), _max_advance(12), _max_digit_advance(12), _max_name_advance(12), _avg_advance(12), _spacing(0), _scale_x(11.0 / 12), _scale_y(1.0), #ifdef TTF _point_size(0), _outline(0), #endif _texture_id() { static const std::array top_1 = { 15, 2, 2, 2, 1, 2, 3, 2, 2, 2, 2, 6, 11, 8, 11, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 6, 6, 6, 7, 6, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 1, -1, 4, 1, 5, 2, 5, 2, 5, 2, 5, 2, 1, 1, 2, 2, 5, 5, 5, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5, 5, 2, 2, 2, 8, 2, 1, 1, 1, 6, 1, 2, 1, 2, 1, 1, 2, 2, 2, -1, -1, -1, 2, 5, 5, 1, 2, 2, -1, 2, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1 }; static const std::array bottom_1 = { 15, 15, 9, 17, 18, 15, 15, 9, 17, 17, 12, 15, 18, 11, 15, 17, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 17, 15, 13, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 18, 15, 15, 15, 15, 15, 15, 15, 15, 15, 17, 17, 17, 6, 17, 6, 15, 15, 15, 15, 15, 15, 18, 15, 15, 18, 15, 15, 15, 15, 15, 18, 18, 15, 15, 15, 15, 15, 15, 15, 18, 15, 19, 19, 19, 12, 15, 15, 15, 15, 19, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 15, 15, 15, 15, 15, 15, 16, 15, 15, 15, 16, 16, 15, 16, 15, 16, 15, 16, 15, 15 }; static const std::array asterisk_centers = { 7, 7, 7, 7, 6, 5, 6 }; size_t font_nr = option.font_number(); std::array char_widths; if (font_nr == 1) { char_widths = { { 4, 2, 7, 11, 8, 12, 12, 2, 7, 7, 9, 10, 3, 8, 2, 10, 10, 10, 8, 8, 10, 7, 9, 9, 9, 9, 3, 3, 10, 10, 10, 9, 12, 12, 9, 10, 10, 9, 9, 10, 9, 8, 7, 11, 8, 11, 10, 11, 9, 11, 11, 9, 10, 9, 12, 12, 12, 12, 10, 6, 10, 6, 10, 12, 3, 11, 9, 9, 9, 9, 8, 9, 9, 4, 6, 10, 4, 11, 9, 10, 9, 9, 8, 8, 8, 9, 10, 12, 10, 10, 9, 8, 2, 8, 10, 8, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 } }; _spacing = 4; } else if (font_nr == 2) { char_widths = { { 8, 8, 8, 10, 8, 10, 10, 8, 8, 8, 8, 10, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10, 10, 10, 8, 12, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 8, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 8, 8, 8, 8, 8, 8, 8, 10, 8, 8, 8, 8, 8, 8, 10, 8, 8, 8, 8, 8, 8, 8, 8, 8, 10, 8, 8, 8, 10, 8, 10, 10, 8, 10, 8, 8, 8, 10, 10, 10, 8, 10, 10, 8, 8, 8, 12, 12, 12, 10, 10, 12, 10, 12, 12 } }; _spacing = 2; } else { std::fill(char_widths.begin(), char_widths.end(), 12); _flags |= Flags::FIXED_WIDTH; } int digit_start = get_position('0'); int digit_end = get_position('9') + 1; int lower_start = get_position('a'); int lower_end = get_position('z') + 1; int upper_start = get_position('A'); int upper_end = get_position('Z') + 1; int underscore_pos = get_position('_'); for (size_t pos = 0; pos < nr_glyphs; ++pos) { int row = pos / font_chars_per_line; int col = pos % font_chars_per_line; int cw = char_widths[pos] + _spacing; int skip = (12 - cw) / 2; _metrics[pos].width = char_widths[pos]; _metrics[pos].advance = char_widths[pos]; _metrics[pos].x_off = 0; _metrics[pos].top = font_nr < 2 ? top_1[pos] : 0; _metrics[pos].bottom = font_nr < 2 ? bottom_1[pos] : _line_height; _metrics[pos].u_start = float(col * font_block_width + skip) / 256; _metrics[pos].v_start = float(row * font_block_height) / 256; _metrics[pos].u_end = float((col+1) * font_block_width - 7 - skip) / 256; _metrics[pos].v_end = float((row+1) * font_block_height - 2) / 256; } _avg_advance = calc_average_advance(); _password_center_offset = asterisk_centers[font_nr]; _max_digit_advance = *std::max_element(char_widths.begin() + digit_start, char_widths.begin() + digit_end); _max_name_advance = std::max({_max_digit_advance, char_widths[underscore_pos], *std::max_element(char_widths.begin() + upper_start, char_widths.begin() + upper_end), *std::max_element(char_widths.begin() + lower_start, char_widths.begin() + lower_end) }); } #ifdef TTF Font::Font(const FontOption& option, int height, bool outline): _font_name(option.font_name()), _file_name(option.file_name()), _flags(IS_TTF), _texture_width(0), _texture_height(0), _metrics(), _block_width(0), _line_height(0), _vertical_advance(0), _font_top_offset(0), _digit_center_offset(0), _password_center_offset(0), _max_advance(0), _max_digit_advance(0), _max_name_advance(0), _avg_advance(0), _spacing(0), _scale_x(1.0), _scale_y(1.0), _point_size(), _outline(std::round(float(height) / (font_block_height - 2))), _texture_id() { _point_size = find_point_size(height); if (option.is_fixed_width()) _flags |= Flags::FIXED_WIDTH; if (outline) _flags |= Flags::HAS_OUTLINE; } #endif Font::~Font() { #ifdef TTF if (is_ttf() && has_texture()) glDeleteTextures(1, &_texture_id.gl_id); #endif } int Font::get_position(unsigned char c) { static const int pos_table[256] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 122, -1, -1, 109, 118, 116, -1, -1, 123, -1, -1, -1, 125, -1, -1, -1, 120, -1, 127, -1, -1, 110, -1, 117, -1, 129, -1, 111, -1, -1, 112, 98, 121, 97, -1, 106, 115, 113, 99, 102, 96, 100, 101, 124, 124, -1, 103, -1, 119, 126, 126, 104, -1, 107, -1, 114, 105, 128, -1, 108, -1, -1, -1 }; return pos_table[c]; } int Font::width_pos(int pos, float zoom) const { if (pos < 0) return 0; return std::round((_metrics[pos].width + _spacing) * _scale_x * zoom); } int Font::advance_spacing_pos(int pos, float zoom) const { if (pos < 0) return 0; // return width of character + spacing between chars (supports variable width fonts) return std::round((_metrics[pos].advance + _spacing) * _scale_x * zoom); } int Font::max_width_spacing(float zoom) const { return std::round((_max_advance + _spacing) * _scale_x * zoom); } int Font::average_width_spacing(float zoom) const { return std::round((_avg_advance + _spacing) * _scale_x * zoom); } int Font::max_digit_width_spacing(float zoom) const { return std::round((_max_digit_advance + _spacing) * _scale_x * zoom); } int Font::max_name_width_spacing(float zoom) const { return std::round((_max_name_advance + _spacing) * _scale_x * zoom); } int Font::line_width(const unsigned char* str, size_t len, float zoom) const { int cur_width = 0; int last_pos = -1; for (size_t i = 0; i < len; ++i) { int pos = get_position(str[i]); if (pos >= 0) { cur_width += advance_spacing_pos(pos, zoom); last_pos = pos; } } cur_width -= advance_spacing_pos(last_pos, zoom) - width_pos(last_pos, zoom); return cur_width; } int Font::line_width_spacing(const unsigned char* str, size_t len, float zoom) const { int cur_width = 0; for (size_t i = 0; i < len; ++i) cur_width += advance_spacing(str[i], zoom); return cur_width; } int Font::height(float zoom) const { return std::round(_line_height * _scale_y * zoom); } int Font::vertical_advance(float zoom, float line_spacing) const { return std::round(_vertical_advance * _scale_y * zoom * line_spacing); } int Font::max_nr_lines(int max_height, float zoom, float line_spacing) const { int line_height = height(zoom); if (max_height < line_height) return 0; int line_skip = vertical_advance(zoom, line_spacing); return 1 + (max_height - line_height) / line_skip; } int Font::text_height(int nr_lines, float zoom, float line_spacing) { if (nr_lines <= 0) return 0; return height(zoom) + (nr_lines-1) * vertical_advance(zoom, line_spacing); } std::pair Font::dimensions(const unsigned char* str, size_t len, float zoom, float line_spacing) const { if (len == 0) return std::make_pair(0, 0); size_t end = memcspn(str, len, reinterpret_cast("\r\n"), 2); int w = line_width(str, end, zoom); int h = height(zoom); size_t off = end + 1; int line_skip = vertical_advance(zoom, line_spacing); while (off < len) { end = memcspn(str + off, len - off, reinterpret_cast("\r\n"), 2); w = std::max(w, line_width(str + off, end, zoom)); h += line_skip; off += end + 1; } return std::make_pair(w, h); } std::pair Font::top_bottom_unscaled(const unsigned char* text, size_t len) { int top = _line_height, bottom = 0; for (size_t i = 0; i < len; ++i) { int pos = get_position(text[i]); if (pos >= 0) { top = std::min(top, _metrics[pos].top); bottom = std::max(bottom, _metrics[pos].bottom); } } return std::make_pair(top, bottom); } std::pair Font::top_bottom(const unsigned char* text, size_t len, float zoom) { int top, bottom; std::tie(top, bottom) = top_bottom_unscaled(text, len); top = std::round(_scale_y * zoom * top); bottom = std::round(_scale_y * zoom * bottom); return std::make_pair(top, bottom); } int Font::center_offset(const unsigned char* text, size_t len, float zoom) { int top, bottom; std::tie(top, bottom) = top_bottom_unscaled(text, len); if (top >= bottom) return 0; return std::round(_scale_y * zoom * 0.5 * (bottom + top - _line_height)); } // Rules for wrapping text: // 1) Soft line breaks take up no space, the cursor should never be on a soft line break. // 2) The cursor can be on a hard line break character, though. Inserting at that position will // insert a new character at that position. So a hard break takes up the width of a cursor, // even though it is invisible. // 3) When possible, lines are broken after the last space. If no space is found on the current // line, the word is broken after the last character that fits on the line. Since the cursor // should never be on a soft break, we can fill the entire line. // 4) We must be able to place the cursor at the end of the text. Therefore, if the last line cannot // fit the cursor, an extra soft break is inserted. std::tuple Font::reset_soft_breaks(const unsigned char *text, size_t text_len, const TextDrawOptions& options, ssize_t cursor, int *max_line_width) { int block_width = std::ceil(_block_width * _scale_x * options.zoom()); int cursor_width = advance_spacing('_', options.zoom()); if (!text || options.max_width() < block_width) return std::make_tuple(ustring(), 0, 0); std::basic_string wrapped_text; size_t start = 0, end, end_last_space = 0, last_start = 0; int nr_lines = 0; int diff_cursor = 0; while (start < text_len) { int cur_width = 0; for (end = start; end < text_len; ++end) { unsigned char c = text[end]; if (c == '\r') { continue; } // We need to be able to place a cursor at this position, so use the maximum of the // cursor width and character width to determine if the character fits. int chr_width = advance_spacing(c, options.zoom()); if (cur_width + block_width <= options.max_width() || cur_width + std::max(chr_width, cursor_width) <= options.max_width()) { cur_width += chr_width; if (c == ' ') { end_last_space = end + 1; } else if (c == '\n') { ++end; break; } } else { // Character won't fit. Split line after the last space. // If not found, break in the middle of the word. if (end_last_space > start) end = end_last_space; break; } } for (size_t i = start; i < end; ++i) { if (text[i] == '\r') { if (ssize_t(i) < cursor) --diff_cursor; } else { wrapped_text.push_back(text[i]); } } ++nr_lines; if (end < text_len && (wrapped_text.empty() || wrapped_text.back() != '\n')) { wrapped_text.push_back('\r'); if (ssize_t(end) <= cursor) ++diff_cursor; } if (max_line_width) { cur_width = line_width(text + start, end - start, options.zoom()); *max_line_width = std::max(*max_line_width, cur_width); } last_start = start; start = end; } // If the message ends on a newline, an extra empty line is printed, but it has not been counted yet. if (!wrapped_text.empty() && wrapped_text.back() == '\n') ++nr_lines; // If a cursor will not fit behind the last line, add an extra line break. int last_line_width = line_width(text + last_start, text_len - last_start, options.zoom()); if (last_line_width + cursor_width > options.max_width()) { wrapped_text.push_back('\r'); ++nr_lines; } return std::make_tuple(wrapped_text, nr_lines, diff_cursor); } bool Font::load_texture() { #ifdef TTF if (is_ttf()) { return build_texture_atlas(); } else { _texture_id.cache_id = ::load_texture_cached(_file_name.c_str(), tt_font); _flags |= Flags::HAS_TEXTURE; return true; } #else // TTF _texture_id = ::load_texture_cached(_file_name.c_str(), tt_font); _flags |= Flags::HAS_TEXTURE; return true; #endif // TTF } void Font::bind_texture() const { #ifdef TTF if (is_ttf()) { ::bind_texture_id(_texture_id.gl_id); } else { ::bind_texture(_texture_id.cache_id); } #else // TTF ::bind_texture(_texture_id); #endif // TTF } void Font::get_texture_coordinates(int pos, float &u_start, float &u_end, float &v_start, float &v_end) const { u_start = _metrics[pos].u_start; v_start = _metrics[pos].v_start; u_end = _metrics[pos].u_end; v_end = _metrics[pos].v_end; } void Font::set_color(int color) { float r = static_cast(colors_list[color].r1) / 255; float g = static_cast(colors_list[color].g1) / 255; float b = static_cast(colors_list[color].b1) / 255; glColor3f(r, g, b); } int Font::draw_char(unsigned char c, int x, int y, float zoom, bool ignore_color) const { if (is_color(c)) { if (!ignore_color) set_color(from_color_char(c)); return 0; } int pos = get_position(c); if (pos < 0) // watch for illegal/non-display characters { return 0; } // There are two widths here: char_width and advance. The first is the width // of the character as drawn on the screen, the second the number of pixels // the pen should advance for drawing the next character. Interestingly, // char_width can be larger than advance, epsecially for bold fonts. For // size calculations, the only relevant quantity is advance, though. int char_width = width_pos(pos, zoom); int advance = advance_spacing_pos(pos, zoom); int char_height = height(zoom); float u_start, u_end, v_start, v_end; get_texture_coordinates(pos, u_start, u_end, v_start, v_end); // Adjust for character offset in the font x += _metrics[pos].x_off; // and place the text from the graphics on the map glTexCoord2f(u_start, v_start); glVertex3i(x, y - _outline, 0); glTexCoord2f(u_start, v_end); glVertex3i(x, y + char_height + _outline, 0); glTexCoord2f(u_end, v_end); glVertex3i(x + char_width, y + char_height + _outline, 0); glTexCoord2f(u_end, v_start); glVertex3i(x + char_width, y - _outline, 0); return advance; } void Font::draw_help_background(int x, int y, int width, int height) const { glDisable(GL_TEXTURE_2D); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_BLEND); glColor4f(0.0f, 0.0f, 0.0f, 0.5f); glBegin(GL_QUADS); glVertex3i(x - 1, y + height, 0); glVertex3i(x - 1, y, 0); glVertex3i(x + width, y, 0); glVertex3i(x + width, y + height, 0); glEnd(); glDisable(GL_BLEND); glEnable(GL_TEXTURE_2D); } void Font::draw_line(const unsigned char* text, size_t len, int x, int y, const TextDrawOptions &options, size_t sel_begin, size_t sel_end) const { if (options.shadow()) { TextDrawOptions new_options = options; new_options.set_shadow(false); int delta = std::round(options.zoom()); if (delta > 0) { new_options.set_ignore_color(); new_options.use_background_color(); draw_line(text, len, x-delta, y-delta, new_options, sel_begin, sel_end); draw_line(text, len, x-delta, y, new_options, sel_begin, sel_end); draw_line(text, len, x-delta, y+delta, new_options, sel_begin, sel_end); draw_line(text, len, x, y+delta, new_options, sel_begin, sel_end); draw_line(text, len, x+delta, y+delta, new_options, sel_begin, sel_end); draw_line(text, len, x+delta, y, new_options, sel_begin, sel_end); draw_line(text, len, x+delta, y-delta, new_options, sel_begin, sel_end); draw_line(text, len, x, y-delta, new_options, sel_begin, sel_end); } new_options.set_ignore_color(false); draw_line(text, len, x, y, new_options, sel_begin, sel_end); } else if (sel_end <= sel_begin || sel_begin >= len || options.ignore_color()) { if (!options.ignore_color() && options.has_foreground_color()) options.use_foreground_color(); int cur_x = x; for (size_t i = 0; i < len; ++i) { cur_x += draw_char(text[i], cur_x, y, options.zoom(), options.ignore_color()); } } else { int cur_x = x; int last_color_char = 0; if (sel_begin > 0) { if (options.has_foreground_color()) options.use_foreground_color(); for (size_t i = 0; i < sel_begin; ++i) { if (is_color(text[i])) last_color_char = text[i]; cur_x += draw_char(text[i], cur_x, y, options.zoom(), false); } } options.use_selection_color(); for (size_t i = sel_begin; i < std::min(sel_end, len); ++i) { if (is_color(text[i])) last_color_char = text[i]; cur_x += draw_char(text[i], cur_x, y, options.zoom(), false); } if (len > sel_end) { if (last_color_char) set_color(from_color_char(last_color_char)); else options.use_foreground_color(); for (size_t i = sel_end; i < len; ++i) { cur_x += draw_char(text[i], cur_x, y, options.zoom(), false); } } } } std::pair Font::clip_line(const unsigned char *text, size_t len, const TextDrawOptions &options, unsigned char &before_color, unsigned char &after_color, int &width) const { int max_width = options.max_width() > 0 ? options.max_width() : window_width; before_color = 0; after_color = 0; width = line_width_spacing(text, len, options.zoom()); if (width <= max_width) // Entire string fits return std::make_pair(0, len); int trunc_width = max_width; if (options.ellipsis()) { int ellipsis_width = line_width_spacing(ellipsis, options.zoom()); if (options.alignment() == TextDrawOptions::Alignment::CENTER) ellipsis_width *= 2; trunc_width -= ellipsis_width; } size_t start = 0, end = len; switch (options.alignment()) { case TextDrawOptions::Alignment::LEFT: { while (end > 0 && width > trunc_width) { unsigned char ch = text[--end]; if (is_color(ch) && !after_color) after_color = ch; else width -= advance_spacing(ch, options.zoom()); } break; } case TextDrawOptions::Alignment::RIGHT: { while (start < len && width > trunc_width) { unsigned char ch = text[start++]; if (is_color(ch)) before_color = ch; else width -= advance_spacing(ch, options.zoom()); } break; } case TextDrawOptions::Alignment::CENTER: { int d_left = 0, d_right = 0; while (start < end && width - d_left - d_right > trunc_width) { if (d_left < d_right) { unsigned char ch = text[start++]; if (is_color(ch)) before_color = ch; else d_left += advance_spacing(ch, options.zoom()); } else { unsigned char ch = text[--end]; if (is_color(ch) && !after_color) after_color = ch; else d_right += advance_spacing(ch, options.zoom()); } } width -= d_left + d_right; break; } } width = max_width; return std::make_pair(start, end-start); } void Font::draw(const unsigned char* text, size_t len, int x, int y, const TextDrawOptions &options, size_t sel_begin, size_t sel_end) const { if (options.shrink_to_fit() && options.max_width() > 0) { int width = dimensions(text, len, options.zoom()).first; if (width > options.max_width()) { TextDrawOptions new_options = TextDrawOptions(options).set_max_width(0) .set_zoom(options.zoom() * float(options.max_width()) / width) .set_shrink_to_fit(false); draw(text, len, x, y, new_options, sel_begin, sel_end); return; } } int tot_width = 0, tot_height = 0; if (options.is_help() || options.vertical_alignment() == TextDrawOptions::VerticalAlignment::BOTTOM_LINE || options.vertical_alignment() == TextDrawOptions::VerticalAlignment::CENTER_LINE) { std::tie(tot_width, tot_height) = dimensions(text, len, options.zoom(), options.line_spacing()); } switch (options.vertical_alignment()) { case TextDrawOptions::VerticalAlignment::TOP_LINE: break; case TextDrawOptions::VerticalAlignment::TOP_FONT: y -= std::round(options.zoom() * _scale_y * _font_top_offset); break; case TextDrawOptions::VerticalAlignment::BOTTOM_LINE: y -= tot_height; break; case TextDrawOptions::VerticalAlignment::CENTER_LINE: y -= tot_height / 2; break; case CENTER_DIGITS: y -= std::round(options.zoom() * _scale_y * _digit_center_offset); break; case CENTER_PASSWORD: y -= std::round(options.zoom() * _scale_y * _password_center_offset); break; } if (options.is_help()) { int x_left = 0; switch (options.alignment()) { case TextDrawOptions::Alignment::LEFT: x_left = x; break; case TextDrawOptions::Alignment::CENTER: x_left = x - tot_width / 2; break; case TextDrawOptions::Alignment::RIGHT: x_left = x - tot_width; break; } draw_help_background(x_left, y, tot_width, tot_height); } #ifdef OPENGL_TRACE CHECK_GL_ERRORS(); #endif //OPENGL_TRACE glEnable(GL_ALPHA_TEST); // enable alpha filtering, so we have some alpha key glAlphaFunc(GL_GREATER, 0.1f); if (is_ttf()) { // Only enable alpha blending for TTF fonts. The old style fonts have an alpha channel, // but this has never been used before except for alpha filtering. Instead, the // semi-transparent border has always been drawn as solid black. glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } bind_texture(); glBegin(GL_QUADS); int line_skip = vertical_advance(options.zoom(), options.line_spacing()); int nr_lines_drawn = 0; size_t start = 0; while (start < len) { size_t line_len = memcspn(text + start, len-start, reinterpret_cast("\r\n"), 2); size_t clipped_off, clipped_line_len; int line_width; unsigned char before_color, after_color; std::tie(clipped_off, clipped_line_len) = clip_line(text + start, line_len, options, before_color, after_color, line_width); bool draw_ellipsis = options.ellipsis() && clipped_line_len < line_len; int ellipsis_width = draw_ellipsis ? line_width_spacing(ellipsis, options.zoom()) : 0; int x_left = 0; switch (options.alignment()) { case TextDrawOptions::Alignment::LEFT: x_left = x; break; case TextDrawOptions::Alignment::CENTER: x_left = x - line_width / 2; break; case TextDrawOptions::Alignment::RIGHT: x_left = x - line_width; break; } if (before_color) set_color(from_color_char(before_color)); int x_draw = x_left; if (draw_ellipsis && options.alignment() != TextDrawOptions::Alignment::LEFT) { draw_line(ellipsis.data(), ellipsis.length(), x_draw, y, options); x_draw += ellipsis_width; } draw_line(text + start + clipped_off, clipped_line_len, x_draw, y, options, sel_begin < start ? 0 : sel_begin - start, sel_end < start ? 0 : sel_end - start); if (draw_ellipsis && options.alignment() != TextDrawOptions::Alignment::RIGHT) { x_draw = x_left + line_width - ellipsis_width; draw_line(ellipsis.data(), ellipsis.length(), x_draw, y, options); } if (after_color) set_color(from_color_char(after_color)); ++nr_lines_drawn; if (options.max_lines() > 0 && nr_lines_drawn >= options.max_lines()) break; y += line_skip; start += line_len + 1; } glEnd(); if (is_ttf()) { glDisable(GL_BLEND); } glDisable(GL_ALPHA_TEST); #ifdef OPENGL_TRACE CHECK_GL_ERRORS(); #endif //OPENGL_TRACE } void Font::draw_messages(const text_message *msgs, size_t msgs_size, int x, int y, Uint8 filter, size_t msg_start, size_t offset_start, const TextDrawOptions &options, ssize_t cursor, select_info* select) const { int block_width = std::ceil(_block_width * _scale_x * options.zoom()); int line_skip = vertical_advance(options.zoom(), options.line_spacing()); int cursor_width = advance_spacing('_', options.zoom()); if (options.max_width() < block_width || options.max_lines() < 1) // no point in trying return; size_t imsg = msg_start; size_t ichar = offset_start; if (filter_messages(msgs, msgs_size, filter, msg_start, imsg, ichar)) // nothing to draw return; unsigned char ch = msgs[imsg].data[ichar]; unsigned char last_color_char = 0; if (!is_color(ch)) { ssize_t i; // search backwards for the last color for (i = ssize_t(ichar) - 1; i >= 0; --i) { ch = msgs[imsg].data[i]; if (is_color(ch)) { set_color(from_color_char(ch)); last_color_char = ch; break; } } if (i < 0 && msgs[imsg].r >= 0) // no color character found, try the message color glColor3f(msgs[imsg].r, msgs[imsg].g, msgs[imsg].b); } glEnable(GL_ALPHA_TEST); // enable alpha filtering, so we have some alpha key glAlphaFunc(GL_GREATER, 0.1f); if (is_ttf()) { // Only enable alpha blending for TTF fonts. The old style fonts have an alpha channel, // but this has never been used before. Instead, the semi-transparent border has always // been drawn as solid black. glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } bind_texture(); int cur_x = x, cur_y = y, cur_line = 0; int cursor_x = x - 1, cursor_y = y - 1; size_t i_total = 0; bool in_select = false; glBegin(GL_QUADS); while (true) { ch = msgs[imsg].data[ichar]; // watch for special characters if (ch == '\0') { // end of message if (++imsg >= msgs_size) imsg = 0; ichar = 0; if (imsg == msg_start || filter_messages(msgs, msgs_size, filter, msg_start, imsg, ichar)) break; last_color_char = 0; } if (select && select->lines && select->lines[cur_line].msg == -1) { select->lines[cur_line].msg = imsg; select->lines[cur_line].chr = ichar; } if (ch == '\n' || ch == '\r' || ch == '\0') { if (i_total == cursor) { // Cursor is on the newline character cursor_x = cur_x; cursor_y = cur_y; } // newline if (++cur_line >= options.max_lines()) break; cur_y += line_skip; cur_x = x; if (ch != '\0') ++ichar; ++i_total; continue; } if (pos_selected(select, imsg, ichar)) { if (!in_select) { glColor3fv(TextDrawOptions::default_selection_color.data()); in_select = true; } } else if (in_select) { if (last_color_char) set_color(from_color_char(last_color_char)); else if (msgs[imsg].r >= 0) glColor3f(msgs[imsg].r, msgs[imsg].g, msgs[imsg].b); else set_color(c_grey1); in_select = false; } if (is_color(ch)) last_color_char = ch; int chr_width = advance_spacing(ch, options.zoom()); if (cur_x - x + chr_width <= options.max_width()) { if (i_total == cursor) { cursor_x = cur_x; cursor_y = cur_y; } cur_x += draw_char(ch, cur_x, cur_y, options.zoom(), in_select); ++ichar; ++i_total; } else { // ignore rest of this line, but keep track of // color characters ++ichar; ++i_total; while (true) { ch = msgs[imsg].data[ichar]; if (ch == '\0' || ch == '\n' || ch == '\r') break; if (is_color(ch)) last_color_char = ch; ++ichar; ++i_total; } } } if (i_total == cursor) { if (cur_x - x + cursor_width <= options.max_width()) { cursor_x = cur_x; cursor_y = cur_y; } else if (cur_line + 1 < options.max_lines()) { cursor_x = x; cursor_y = cur_y + line_skip; } } if (cursor_x >= x && cursor_y >= y) { draw_char('_', cursor_x, cursor_y, options.zoom(), true); } glEnd(); if (is_ttf()) { glDisable(GL_BLEND); } glDisable(GL_ALPHA_TEST); #ifdef OPENGL_TRACE CHECK_GL_ERRORS(); #endif //OPENGL_TRACE } void Font::draw_console_separator(int x_space, int y, const TextDrawOptions& options) const { int pos = get_position('^'); int char_width = width_pos(pos, options.zoom()); int char_height = height(options.zoom()); int dx = advance_spacing_pos(pos, options.zoom()); float u_start, u_end, v_start, v_end; get_texture_coordinates(pos, u_start, u_end, v_start, v_end); glEnable(GL_ALPHA_TEST); // enable alpha filtering, so we have some alpha key glAlphaFunc(GL_GREATER, 0.1f); bind_texture(); glBegin(GL_QUADS); int x = x_space; while (x + char_width <= options.max_width()) { glTexCoord2f(u_start, v_start); glVertex3i(x, y, 0); glTexCoord2f(u_start, v_end); glVertex3i(x, y + char_height, 0); glTexCoord2f(u_end, v_end); glVertex3i(x + char_width, y + char_height, 0); glTexCoord2f(u_end, v_start); glVertex3i(x + char_width, y, 0); x += dx; if (x + char_width > options.max_width()) break; glTexCoord2f(u_start, v_start); glVertex3i(x, y, 0); glTexCoord2f(u_start, v_end); glVertex3i(x, y + char_height, 0); glTexCoord2f(u_end, v_end); glVertex3i(x + char_width, y + char_height, 0); glTexCoord2f(u_end, v_start); glVertex3i(x + char_width, y, 0); x += 2 * dx; } glEnd(); glDisable(GL_ALPHA_TEST); #ifdef OPENGL_TRACE CHECK_GL_ERRORS(); #endif //OPENGL_TRACE } #ifdef ELC #ifndef MAP_EDITOR2 void Font::draw_ortho_ingame_string(const unsigned char* text, size_t len, float x, float y, float z, int max_lines, float zoom_x, float zoom_y) const { int char_height = height(zoom_y); int line_skip = vertical_advance(zoom_y); float cur_x = x; float cur_y = y; int cur_line = 0; glEnable(GL_ALPHA_TEST); // enable alpha filtering, so we have some alpha key glAlphaFunc(GL_GREATER, 0.1f); if (is_ttf()) { // Only enable alpha blending for TTF fonts. The old style fonts have an alpha channel, // but this has never been used before. Instead, the semi-transparent border has always // been drawn as solid black. glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } bind_texture(); glBegin(GL_QUADS); for (size_t i = 0; i < len; ++i) { unsigned char ch = text[i]; if (ch == '\n' || ch == '\r') { cur_y -= line_skip; cur_x = x; if (++cur_line >= max_lines) break; } else if (is_color(ch)) { glEnd(); // Ooops - NV bug fix!! set_color(from_color_char(ch)); glBegin(GL_QUADS); } else { int pos = get_position(ch); if (pos >= 0) { int char_width = width_pos(pos, zoom_x); float u_start, u_end, v_start, v_end; get_texture_coordinates(pos, u_start, u_end, v_start, v_end); float x_left = cur_x + _metrics[pos].x_off; glTexCoord2f(u_start, v_start); glVertex3f(x_left, cur_y+char_height+_outline, z); glTexCoord2f(u_start, v_end); glVertex3f(x_left, cur_y-_outline, z); glTexCoord2f(u_end, v_end); glVertex3f(x_left+char_width, cur_y-_outline, z); glTexCoord2f(u_end, v_start); glVertex3f(x_left+char_width, cur_y+char_height+_outline, z); cur_x += advance_spacing_pos(pos, zoom_x); } } } glEnd(); if (is_ttf()) glDisable(GL_BLEND); glDisable(GL_ALPHA_TEST); } #endif // !MAP_EDITOR_2 #endif // ELC #ifdef TTF bool Font::render_glyph(size_t i_glyph, int size, int y_delta, int outline_size, TTF_Font *font, SDL_Surface *surface) { static const Uint16 glyphs[nr_glyphs] = { ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', 252, 233, 226, // üéâ' 224, 231, 234, 235, 232, 239, 244, 249, 228, 246, 252, 196, 214, 220, // àçêëèïôùäöüÄÖÜ 223, 230, 248, 229, 198, 216, 197, 241, 209, 225, 193, 201, 237, 205, // ßæøåÆØÅñÑáÁÉíÍ 243, 211, 250, 218, 251, 238 // óÓúÚûî }; static const SDL_Color white = { .r = 0xff, .g = 0xff, .b = 0xff, .a = 0xff }; static const SDL_Color black = { .r = 0x00, .g = 0x00, .b = 0x00, .a = 0xff }; Uint16 glyph = glyphs[i_glyph]; if (!TTF_GlyphIsProvided(font, glyph)) { LOG_ERROR("Font '%s' does not provide glyph for code point '%d': %s", _font_name.c_str(), glyph, TTF_GetError()); return false; } SDL_Surface* glyph_surface; if (outline_size > 0) { TTF_SetFontOutline(font, outline_size); glyph_surface = TTF_RenderGlyph_Blended(font, glyph, black); if (!glyph_surface) { const char* locale = setlocale(LC_ALL, ""); LOG_ERROR("Failed to render outline for TTF glyph '%lc' in font \"%s\": %s", glyphs[i_glyph], _font_name.c_str(), TTF_GetError()); setlocale(LC_ALL, locale); return false; } TTF_SetFontOutline(font, 0); SDL_Surface *fg_surface = TTF_RenderGlyph_Blended(font, glyph, white); if (!fg_surface) { const char* locale = setlocale(LC_ALL, ""); LOG_ERROR("Failed to render TTF glyph '%lc' in font \"%s\": %s", glyphs[i_glyph], _font_name.c_str(), TTF_GetError()); setlocale(LC_ALL, locale); return false; } SDL_Rect rect = {outline_size, outline_size, fg_surface->w, fg_surface->h}; SDL_SetSurfaceBlendMode(glyph_surface, SDL_BLENDMODE_BLEND); SDL_BlitSurface(fg_surface, NULL, glyph_surface, &rect); SDL_FreeSurface(fg_surface); } else { glyph_surface = TTF_RenderGlyph_Blended(font, glyph, white); if (!glyph_surface) { const char* locale = setlocale(LC_ALL, ""); LOG_ERROR("Failed to render TTF glyph '%lc' in font \"%s\": %s", glyphs[i_glyph], _font_name.c_str(), TTF_GetError()); setlocale(LC_ALL, locale); return false; } } int width = glyph_surface->w; int height = glyph_surface->h; int row = i_glyph / font_chars_per_line; int col = i_glyph % font_chars_per_line; // Reserve space for the outline, even when it is not used int row_height = size + 2 * _outline + 2; SDL_Rect area; area.x = col*size; area.y = row*row_height + 1 + y_delta + _outline - outline_size; area.w = width; area.h = height; SDL_SetSurfaceAlphaMod(glyph_surface, 0xFF); SDL_SetSurfaceBlendMode(glyph_surface, SDL_BLENDMODE_NONE); int err = SDL_BlitSurface(glyph_surface, NULL, surface, &area); SDL_FreeSurface(glyph_surface); if (err) { LOG_ERROR("Failed to write glyph to surface: %s", SDL_GetError()); return false; } int y_min, y_max, x_min; _metrics[i_glyph].width = width; TTF_GlyphMetrics(font, glyph, &x_min, nullptr, &y_min, &y_max, &_metrics[i_glyph].advance); _metrics[i_glyph].x_off = std::min(x_min, 0); _metrics[i_glyph].top = y_delta + TTF_FontAscent(font) - y_max; _metrics[i_glyph].bottom = y_delta + TTF_FontAscent(font) - y_min; _metrics[i_glyph].u_start = float(col * size) / surface->w; _metrics[i_glyph].v_start = float(row * row_height + 1) / surface->h; _metrics[i_glyph].u_end = float(col * size + width) / surface->w; _metrics[i_glyph].v_end = float(row * row_height + row_height - 1) / surface->h; return true; } int Font::find_point_size(int height) { // If the point size is too small, we may run into problems later where the size of the // glyph becomes zero. This leads to an error in the generation of the texture atlas, which // will reset the font to the default font. It is hard to distinguish such errors, where the // font size is simply too small, from real rendering issues. So we cap the point size at a // minimum of 6, which seems to be a safe limit. static const int min_point_size = 6; int min = min_point_size, max = 2 * height; if (max <= min) { // Height is too small. Check if the font will open at the minimum point size. If not // return 0 to use the default point size and hope for the best. TTF_Font *font = open_font(_file_name.c_str(), min); if (!font) return 0; TTF_CloseFont(font); return min; } while (max > min + 1) { int mid = (min + max) / 2; TTF_Font *font = open_font(_file_name.c_str(), mid); if (!font) return 0; int size = TTF_FontLineSkip(font); TTF_CloseFont(font); if (size == height) return mid; else if (size < height) min = mid; else max = mid; } return max; } bool Font::build_texture_atlas() { static const int nr_rows = (nr_glyphs + font_chars_per_line-1) / font_chars_per_line; if (!is_ttf()) { LOG_ERROR("Font '%s' is not a TrueType font, no need to build a texture", _font_name.c_str()); return false; } int point_size = _point_size ? _point_size : ttf_point_size; TTF_Font *font = open_font(_file_name.c_str(), point_size); if (!font) { LOG_ERROR("Failed to open TrueType font %s: %s", _file_name.c_str(), TTF_GetError()); _flags |= Flags::FAILED; return false; } int outline_size = has_outline() ? _outline : 0; int size = TTF_FontLineSkip(font); int width = next_power_of_two(font_chars_per_line * size); // Keep two rows of empty pixels (one top, one bottom) around the glyphs for systems with an // alternative view on texture coordinates (i.e. off by one pixel). Perhaps they may lose a // single pixel row of a really high character, but at least we won't draw part of the character // bwlow or above it. int height = next_power_of_two(nr_rows * (size + 2 * _outline + 2)); SDL_Surface *image = SDL_CreateRGBSurface(SDL_SWSURFACE, width, height, 32, #if SDL_BYTEORDER == SDL_LIL_ENDIAN /* OpenGL RGBA masks */ 0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000 #else 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF #endif ); if (!image) { LOG_ERROR("Failed to create surface for TTF texture atlas: %s", TTF_GetError()); TTF_CloseFont(font); _flags |= Flags::FAILED; return false; } int y_delta = (size - TTF_FontHeight(font)) / 2; for (size_t i_glyph = 0; i_glyph < nr_glyphs; ++i_glyph) { if (!render_glyph(i_glyph, size, y_delta, outline_size, font, image)) { SDL_FreeSurface(image); TTF_CloseFont(font); _flags |= Flags::FAILED; return false; } } GLuint texture_id; glGenTextures(1, &texture_id); ::bind_texture_id(texture_id); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image->pixels); SDL_FreeSurface(image); _texture_id.gl_id = texture_id; _texture_width = width; _texture_height = height; _block_width = size; _line_height = _vertical_advance = size; _font_top_offset = y_delta; int digit_start = get_position('0'); int digit_end = get_position('9') + 1; int lower_start = get_position('a'); int lower_end = get_position('z') + 1; int upper_start = get_position('A'); int upper_end = get_position('Z') + 1; int underscore_pos = get_position('_'); int digit_top = std::min_element(_metrics.begin() + digit_start, _metrics.begin() + digit_end, [](const Metrics& m0, const Metrics& m1) { return m0.top < m1.top; })->top; int digit_bottom = std::max_element(_metrics.begin() + digit_start, _metrics.begin() + digit_end, [](const Metrics& m0, const Metrics& m1) { return m0.bottom < m1.bottom; })->bottom; _digit_center_offset = (digit_top + digit_bottom) / 2; int pos = get_position('*'); _password_center_offset = (_metrics[pos].top + _metrics[pos].bottom) / 2; _max_advance = std::max_element(_metrics.begin(), _metrics.end(), [](const Metrics& m0, const Metrics& m1) { return m0.advance < m1.advance; })->advance; _max_digit_advance = std::max_element(_metrics.begin() + digit_start, _metrics.begin() + digit_end, [](const Metrics& m0, const Metrics& m1) { return m0.advance < m1.advance; })->advance; _max_name_advance= std::max({_max_digit_advance, _metrics[underscore_pos].advance, std::max_element(_metrics.begin() + upper_start, _metrics.begin() + upper_end, [](const Metrics& m0, const Metrics& m1) { return m0.advance < m1.advance; })->advance, std::max_element(_metrics.begin() + lower_start, _metrics.begin() + lower_end, [](const Metrics& m0, const Metrics& m1) { return m0.advance < m1.advance; })->advance }); _avg_advance = calc_average_advance(); _scale_x = _scale_y = float(font_block_height - 2) / size; _flags |= Flags::HAS_TEXTURE; TTF_CloseFont(font); return true; } #endif // TTF int Font::calc_average_advance() { int total = std::accumulate(letter_freqs.begin(), letter_freqs.end(), 0); int dot = std::inner_product(letter_freqs.begin(), letter_freqs.end(), _metrics.begin(), 0, std::plus(), [](int f, const Metrics& m) { return f * m.advance; }); return std::round(float(dot) / total); } const std::array FontManager::_default_font_idxs = { 0, 0, 0, 2, 0, 3, 0, 0 }; std::array FontManager::font_idxs = { 0, 0, 0, 2, 0, 3, 0, 0, 0 }; std::array FontManager::font_scales = { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 }; bool FontManager::initialize() { _options.clear(); _fonts.clear(); for (size_t i = 0; i < _nr_bundled_fonts; ++i) { FontOption option(i); if (!option.failed()) _options.push_back(std::move(option)); } if (_options.empty() || _options[0].failed()) return false; #ifdef TTF initialize_ttf(); #endif add_select_options(); _fonts.insert(std::make_pair(0, Font(_options[0]))); return true; } #ifdef TTF void FontManager::initialize_ttf() { static const char* patterns[] = { "*.ttf", "*.ttc", "*.otf" }; if (!use_ttf) return; if (!TTF_WasInit() && TTF_Init() == -1) { LOG_ERROR("Failed to initialize True Type fonts: %s", TTF_GetError()); use_ttf = 0; return; } size_t nr_existing_fonts = _options.size(); // First load from the game fonts directory std::string fonts_dir = std::string(datadir) + "/fonts"; for (const char* pattern: patterns) { search_files_and_apply(fonts_dir.c_str(), pattern, [](const char *fname_ptr) { size_t dir_name_len = strlen(datadir); std::string fname; if (!strncmp(fname_ptr, datadir, dir_name_len)) fname = fname_ptr + dir_name_len; else fname = fname_ptr; FontOption option(fname); if (!option.failed()) FontManager::get_instance()._options.push_back(std::move(option)); }, 0); } // Now search files in the user specified fonts directory for (const char* pattern: patterns) { search_files_and_apply(ttf_directory, pattern, [](const char *fname_ptr) { size_t dir_name_len = strlen(ttf_directory); std::string fname; if (!strncmp(fname_ptr, ttf_directory, dir_name_len)) fname = fname_ptr + dir_name_len; else fname = fname_ptr; FontOption option(fname); if (!option.failed()) FontManager::get_instance()._options.push_back(std::move(option)); }, 1); } // Remove duplicate fonts, possibly caused by multiple copies of the same font in different // directories. A font is assumed to be a duplicate of another if both the base name of the // file and the font name are equal. std::sort(_options.begin() + nr_existing_fonts, _options.end(), [](const FontOption& f0, const FontOption& f1) { if (f0.file_base_name() == f1.file_base_name()) return f0.font_name() < f1.font_name(); else return f0.file_base_name() < f1.file_base_name(); }); auto new_end = std::unique(_options.begin(), _options.end(), [](const FontOption& f0, const FontOption& f1) { return f0.file_base_name() == f1.file_base_name() && f0.font_name() == f1.font_name(); }); _options.erase(new_end, _options.end()); // Sort TTF fonts by font name, but keep them after EL bundled fonts std::sort(_options.begin() + nr_existing_fonts, _options.end(), [](const FontOption& f0, const FontOption& f1) { return f0.font_name() < f1.font_name(); }); } #endif // TTF void FontManager::add_select_options(bool add_button) { clear_multiselect_var("ui_font"); clear_multiselect_var("chat_font"); clear_multiselect_var("name_font"); clear_multiselect_var("book_font"); clear_multiselect_var("note_font"); clear_multiselect_var("rules_font"); clear_multiselect_var("encyclopedia_font"); _fixed_width_idxs.clear(); for (size_t i = 0; i < _options.size(); ++i) { const FontOption& option = _options[i]; if (option.is_fixed_width()) _fixed_width_idxs.push_back(i); option.add_select_options(add_button); } if (add_button) { set_multiselect_var("ui_font", font_idxs[UI_FONT], true); set_multiselect_var("chat_font", font_idxs[CHAT_FONT], true); set_multiselect_var("name_font", font_idxs[NAME_FONT], true); set_multiselect_var("book_font", font_idxs[BOOK_FONT], true); set_multiselect_var("note_font", font_idxs[NOTE_FONT], true); set_multiselect_var("rules_font", font_idxs[RULES_FONT], true); auto it = std::find(_fixed_width_idxs.begin(), _fixed_width_idxs.end(), font_idxs[ENCYCLOPEDIA_FONT]); if (it != _fixed_width_idxs.end()) set_multiselect_var("encyclopedia_font", it - _fixed_width_idxs.begin(), true); } } static uint32_t get_key(size_t idx, int height, bool outline) { // It is unlikely that there will be more than 64k fonts, or that the line height wil be more // than 32k pixels, so combine the three values into a single key 32-bit key. return (outline << 31) | ((height & 0x7fff) << 16) | (idx & 0xffff); } Font& FontManager::get(Category cat, float text_zoom) { size_t idx = font_idxs[cat]; bool outline = cat != BOOK_FONT; // Don't draw an outline for book text if (idx > _options.size()) { #ifdef TTF // Invalid font number if (cat == CONFIG_FONT) { // Probably TTF was disabled, and the settings window was using a TTF font. Check if // it is still available int height = std::round((Font::font_block_height - 2) * text_zoom * font_scales[cat]); uint32_t key = get_key(0xffff, height, outline); auto it = _fonts.find(key); if (it != _fonts.end()) return it->second; else font_idxs[cat] = idx = 0; } else { font_idxs[cat] = idx = 0; } #else // TTF font_idxs[cat] = idx = 0; #endif // TTF } #ifdef TTF // Bundled fonts come in only one size, so always use 0 for the line height and rely on scaling // to display the correct size. int height = _options[idx].is_ttf() ? std::round((Font::font_block_height - 2) * text_zoom * font_scales[cat]) : 0; uint32_t key = get_key(idx, height, outline); auto it = _fonts.find(key); if (it == _fonts.end()) { // The font has not been loaded yet Font font = _options[idx].is_ttf() ? Font(_options[idx], height, outline) : Font(_options[idx]); it = _fonts.insert(std::make_pair(key, std::move(font))).first; } #else // TTF uint32_t key = idx; auto it = _fonts.find(idx); if (it == _fonts.end()) { // The font has not been loaded yet Font font = Font(_options[idx]); it = _fonts.insert(std::make_pair(key, std::move(font))).first; } #endif // TTF Font *result = &it->second; if (result->failed()) { // Failed to load previously, switch to fixed font_idxs[cat] = 0; return _fonts.at(0); } if (!result->has_texture() && !result->load_texture()) { // Failed to load or generate texture, switch to fixed font_idxs[cat] = 0; return _fonts.at(0); } return *result; } #ifdef TTF void FontManager::disable_ttf() { // If not yet initialized, do nothing if (!is_initialized()) return; size_t config_font_idx = font_idxs[CONFIG_FONT]; // Save the file names of the fonts currently in use for (size_t i = 0; i < NR_FONT_CATS; ++i) { size_t font_num = font_idxs[i]; if (font_num >= _options.size()) // Invalid index, possibly configuration font after disabling TTF previously font_num = 0; _saved_font_files[i] = _options[font_num].file_name(); if (_options[font_num].is_ttf()) { // Revert to default bundled font font_idxs[i] = (i == CONFIG_FONT ? 0xffff : _default_font_idxs[i]); } } // Remove the font options auto first_ttf = std::find_if(_options.begin(), _options.end(), [](const FontOption& opt) { return opt.is_ttf(); }); _options.erase(first_ttf, _options.end()); // Remove the fonts themselves std::unordered_map config_fonts; for (auto it = _fonts.begin(); it != _fonts.end(); ) { if (it->second.is_ttf()) { if ((it->first & 0xffff) == config_font_idx) { config_fonts.insert(std::make_pair((it->first & 0xffff0000) | 0xffff, std::move(it->second))); } it = _fonts.erase(it); } else { ++it; } } // Reinstate config font with invalid index for (auto it = config_fonts.begin(); it != config_fonts.end(); ++it) _fonts.insert(std::make_pair(it->first, std::move(it->second))); // Remove the TTF font options from the selection add_select_options(true); } void FontManager::enable_ttf() { // If not yet initialized, do nothing if (!is_initialized()) return; // Add TrueType fonts to the font list initialize_ttf(); // Check if we saved font file names when we disabled TTF, and if so, // restore them. Only restore TTF fonts, keeping any changes in bundled // fonts. for (size_t i = 0; i < NR_FONT_CATS; ++i) { // Font for the settings window should not change, it will be updated and set to the // UI font when the window is closed and reopened if (i == CONFIG_FONT) continue; const std::string& fname = _saved_font_files[i]; if (!fname.empty()) { std::vector::const_iterator it = std::find_if(_options.begin(), _options.end(), [fname](const FontOption& opt) { return opt.file_name() == fname; }); if (it != _options.end() && it->is_ttf()) font_idxs[i] = it - _options.begin(); } } // Add selection options for the new fonts add_select_options(true); } #endif // TTF } // namespace eternal_lands extern "C" { using namespace eternal_lands; size_t *font_idxs = FontManager::font_idxs.data(); float *font_scales = FontManager::font_scales.data(); #ifdef TTF int use_ttf = 0; #ifdef LINUX char ttf_directory[TTF_DIR_SIZE] = "/usr/share/fonts/TTF"; #elif defined WINDOWS char ttf_directory[TTF_DIR_SIZE] = "C:/Windows/Fonts"; #elif defined OSX char ttf_directory[TTF_DIR_SIZE] = "/System/Library/Fonts"; #else char ttf_directory[TTF_DIR_SIZE]; #endif // #endif // TTF int initialize_fonts() { return FontManager::get_instance().initialize(); } size_t get_fixed_width_font_number(size_t idx) { return FontManager::get_instance().fixed_width_font_number(idx); } int get_char_width_zoom(unsigned char c, font_cat cat, float zoom) { return FontManager::get_instance().advance_spacing(cat, c, zoom); } int get_max_char_width_zoom(font_cat cat, float zoom) { return FontManager::get_instance().max_width_spacing(cat, zoom); } int get_avg_char_width_zoom(font_cat cat, float zoom) { return FontManager::get_instance().average_width_spacing(cat, zoom); } int get_max_digit_width_zoom(font_cat cat, float zoom) { return FontManager::get_instance().max_digit_width_spacing(cat, zoom); } int get_max_name_width_zoom(font_cat cat, float zoom) { return FontManager::get_instance().max_name_width_spacing(cat, zoom); } int get_buf_width_zoom(const unsigned char* str, size_t len, font_cat cat, float zoom) { return FontManager::get_instance().line_width(static_cast(cat), str, len, zoom); } int get_line_height(font_cat cat, float text_zoom) { return FontManager::get_instance().line_height(cat, text_zoom); } int get_line_skip(font_cat cat, float text_zoom) { return FontManager::get_instance().vertical_advance(cat, text_zoom); } int get_max_nr_lines(int height, font_cat cat, float zoom) { return FontManager::get_instance().max_nr_lines(cat, height, zoom); } int get_text_height(int nr_lines, font_cat cat, float zoom) { return FontManager::get_instance().text_height(cat, nr_lines, zoom); } void get_buf_dimensions(const unsigned char* str, size_t len, font_cat cat, float text_zoom, int *width, int *height) { auto dims = FontManager::get_instance().dimensions(cat, str, len, text_zoom); *width = dims.first; *height = dims.second; } int get_center_offset(const unsigned char* text, size_t len, font_cat cat, float zoom) { return FontManager::get_instance().center_offset(cat, text, len, zoom); } void get_top_bottom(const unsigned char* text, size_t len, font_cat cat, float zoom, int *top, int *bottom) { auto coords = FontManager::get_instance().top_bottom(cat, text, len, zoom); *top = coords.first; *bottom = coords.second; } int reset_soft_breaks(unsigned char *text, int len, int size, font_cat cat, float text_zoom, int width, int *cursor_ptr, int *max_line_width) { int cursor = cursor_ptr ? *cursor_ptr : -1; TextDrawOptions options = TextDrawOptions().set_max_width(width).set_zoom(text_zoom); ustring wrapped_text; int nr_lines, cursor_diff; std::tie(wrapped_text, nr_lines, cursor_diff) = FontManager::get_instance() .reset_soft_breaks(cat, text, len, options, cursor, max_line_width); size_t new_len = wrapped_text.copy(text, size_t(size)-1); text[new_len] = '\0'; if (cursor_ptr) *cursor_ptr = std::min(cursor + cursor_diff, size - 1); return nr_lines; } void put_small_colored_text_in_box_zoomed(int color, const unsigned char* text, int len, int width, unsigned char* buffer, float text_zoom) { TextDrawOptions options = TextDrawOptions().set_max_width(width) .set_zoom(text_zoom * DEFAULT_SMALL_RATIO); ustring wrapped_text; int nr_lines, cursor_diff; std::tie(wrapped_text, nr_lines, cursor_diff) = FontManager::get_instance() .reset_soft_breaks(FontManager::Category::UI_FONT, text, len, options, -1, nullptr); // If we don't end on a newline, add one. // TODO: check if that's necessary. if (wrapped_text.back() != '\n') wrapped_text.push_back('\n'); size_t new_len = 0; if (!is_color(wrapped_text.front())) buffer[new_len++] = to_color_char(color); // FIXME: no size specified for either buffer. Pass unlimited size, // and hope for the best... new_len += wrapped_text.copy(buffer + new_len, ustring::npos); buffer[new_len] = '\0'; } void vdraw_text(int x, int y, const unsigned char* text, size_t len, font_cat cat, va_list options) { TextDrawOptions tdo; text_draw_option_sel sel; int sel_begin = 0, sel_end = 0; bool end_reached = false; do { sel = text_draw_option_sel(va_arg(options, int)); switch (sel) { case TDO_MAX_WIDTH: tdo.set_max_width(va_arg(options, int)); break; case TDO_MAX_LINES: tdo.set_max_lines(va_arg(options, int)); break; case TDO_ZOOM: tdo.set_zoom(va_arg(options, double)); break; case TDO_LINE_SPACING: tdo.set_line_spacing(va_arg(options, double)); break; case TDO_ALIGNMENT: tdo.set_alignment(TextDrawOptions::Alignment(va_arg(options, int))); break; case TDO_VERTICAL_ALIGNMENT: tdo.set_vertical_alignment(TextDrawOptions::VerticalAlignment(va_arg(options, int))); break; case TDO_SHADOW: tdo.set_shadow(va_arg(options, int)); break; case TDO_FOREGROUND: { float r = va_arg(options, double); float g = va_arg(options, double); float b = va_arg(options, double); tdo.set_foreground(r, g, b); break; } case TDO_BACKGROUND: { float r = va_arg(options, double); float g = va_arg(options, double); float b = va_arg(options, double); tdo.set_background(r, g, b); break; } case TDO_SELECTION: { float r = va_arg(options, double); float g = va_arg(options, double); float b = va_arg(options, double); tdo.set_selection(r, g, b); break; } case TDO_IGNORE_COLOR: tdo.set_ignore_color(va_arg(options, int)); break; case TDO_HELP: tdo.set_help(va_arg(options, int)); break; case TDO_ELLIPSIS: tdo.set_ellipsis(va_arg(options, int)); break; case TDO_SHRINK_TO_FIT: tdo.set_shrink_to_fit(va_arg(options, int)); break; case TDO_SEL_BEGIN: sel_begin = va_arg(options, int); break; case TDO_SEL_END: sel_end = va_arg(options, int); break; case TDO_END: end_reached = true; break; default: LOG_ERROR("Unknown text draw option selector %d", sel); end_reached = true; break; } } while (!end_reached); FontManager::get_instance().draw(cat, text, len, x, y, tdo, sel_begin, sel_end); } void draw_string_zoomed_centered_around(int x, int y, const unsigned char *text, int center_idx, float text_zoom) { size_t len = strlen(reinterpret_cast(text)); size_t idx = center_idx; TextDrawOptions options = TextDrawOptions().set_zoom(text_zoom) .set_alignment(TextDrawOptions::Alignment::RIGHT); FontManager::get_instance().draw(FontManager::Category::UI_FONT, text, std::min(len, idx), x, y, options); if (idx < len) { options.set_alignment(TextDrawOptions::Alignment::LEFT); FontManager::get_instance().draw(FontManager::Category::UI_FONT, text + idx, len - idx, x, y, options); } } void draw_messages(int x, int y, text_message *msgs, int msgs_size, Uint8 filter, int msg_start, int offset_start, int cursor, int width, int height, font_cat cat, float text_zoom, select_info* select) { int max_nr_lines = FontManager::get_instance().max_nr_lines(cat, height, text_zoom); TextDrawOptions options = TextDrawOptions().set_max_width(width) .set_max_lines(max_nr_lines) .set_zoom(text_zoom); FontManager::get_instance().draw_messages(cat, msgs, msgs_size, x, y, filter, msg_start, offset_start, options, cursor, select); } void draw_console_separator(int x_space, int y, int width, float text_zoom) { TextDrawOptions options = TextDrawOptions().set_max_width(width).set_zoom(text_zoom); FontManager::get_instance().draw_console_separator(CHAT_FONT, x_space, y, options); } #ifdef ELC #ifndef MAP_EDITOR2 void draw_ortho_ingame_string(float x, float y, float z, const unsigned char *text, int max_lines, font_cat cat, float zoom_x, float zoom_y) { FontManager::get_instance().draw_ortho_ingame_string(cat, text, strlen(reinterpret_cast(text)), x, y, z, max_lines, zoom_x, zoom_y); } #endif // !MAP_EDITOR2 #endif // ELC void set_config_font(void) { FontManager::get_instance().set_config_font(); } int has_glyph(unsigned char c) { return Font::has_glyph(c); } #ifdef TTF void disable_ttf(void) { FontManager::get_instance().disable_ttf(); } void enable_ttf(void) { FontManager::get_instance().enable_ttf(); } #endif // TTF } //extern "C"