// // render_text.cpp // BoE // // Created by Celtic Minstrel on 17-04-14. // // #include "render_text.hpp" #include #include #include #include "fileio/resmgr/res_font.hpp" #include "gfx/render_shapes.hpp" #include #include "winutil.hpp" sf::Font& get_font_rsrc(eFont font) { switch(font) { case FONT_PLAIN: return *ResMgr::fonts.get("plain"); case FONT_BOLD: return *ResMgr::fonts.get("bold"); case FONT_DUNGEON: return *ResMgr::fonts.get("dungeon"); case FONT_MAIDWORD: return *ResMgr::fonts.get("maidenword"); } } void TextStyle::applyTo(sf::Text& text, double scale) const { // Guarantee the font texture for the needed text size won't need to be resized in the middle of rendering sf::Font& font_obj = get_font_rsrc(font); int size = pointSize * scale; sf::Texture& font_texture = const_cast(font_obj.getTexture(size)); int min_texture_size = 128 * scale; if(font_texture.getSize().x < min_texture_size){ int texture_scale = 2; while(128 * texture_scale < min_texture_size){ texture_scale << 1; } if(!font_texture.create(128 * texture_scale, 128 * texture_scale)){ throw std::string { "Failed to create large enough font texture!" }; } } text.setFont(font_obj); text.setCharacterSize(size); int style = sf::Text::Regular; if(italic) style |= sf::Text::Italic; if(underline) style |= sf::Text::Underlined; text.setStyle(style); text.setColor(colour); } struct text_params_t { TextStyle style; eTextMode mode; bool showBreaks = false; // Hilite ranges are, like the STL, of the form [first, last). std::vector hilite_ranges; sf::Color hilite_fg, hilite_bg = sf::Color::Transparent; enum {RECTS, SNIPPETS} returnType; std::vector returnRects; std::vector snippets; // Pre-calculated line wrapping: break_info_t break_info; bool right_align = false; }; static void push_snippets(size_t start, size_t end, text_params_t& options, size_t& iHilite, const std::string& str, location loc) { std::vector& hilites = options.hilite_ranges; std::vector& snippets = options.snippets; // Check if we have any hilites on this line. // We assume the list of hilites is sorted, so we just need to // check the current entry. size_t upper_bound = end; do { bool hilited = false; // Skip any hilite rules that have start = end while(iHilite < hilites.size() && hilites[iHilite].first == hilites[iHilite].second) iHilite++; if(iHilite < hilites.size()) { // Possibilities: (vertical bars indicate line boundaries, parentheses for hilite boundaries, dots are data) // 1. |...|...(...) --> no hilite on this line // 2a. |...(...|...) --> hilite starts on this line and continues past it // 2b. |...(...)...| --> hilite starts and ends on this line // 3a. (...|...)...| --> hilite starts (or continues) at the start of the line and ends on this line // 3b. (...|......)| --> entire line hilited // Case 1 needs no special handling; check case 3, then case 2. if(hilites[iHilite].first <= start) { hilited = true; if(hilites[iHilite].second < end) // 3a end = hilites[iHilite].second; } else if(hilites[iHilite].first < end) end = hilites[iHilite].first; // 2b will reduce to 3a after shifting start over } size_t amount = end - start; snippets.push_back({str.substr(start,amount), loc, hilited}); // if(hilited) std::cout << "Hiliting passage : \"" << snippets.back().text << '"' << std::endl; loc.x += string_length(snippets.back().text, options.style); start = end; end = upper_bound; if(iHilite < hilites.size() && start >= hilites[iHilite].second) iHilite++; } while(start < upper_bound); } break_info_t calculate_line_wrapping(rectangle dest_rect, std::string str, TextStyle style) { break_info_t break_info; if(str.empty()) return break_info; // Nothing to do! sf::Text str_to_draw; style.applyTo(str_to_draw); short str_len = str.length(); unsigned short last_line_break = 0,last_word_break = 0; str_to_draw.setString(str); // Even if the text is only one line, break_info is required for calculating word boundaries. // So we can't skip the rest of this. short len = 0; std::vector lengths; sf::Font& font = get_font_rsrc(style.font); // getGlyph() requires Uint32 input, which I don't think char is guaranteed to be const sf::String& sf_str = str_to_draw.getString(); // if i > 0, this lambda must be called with i-1 before calling it with i for the first time auto text_len = [&sf_str, &str_len, &font, &style, &len, &lengths](size_t i) -> int { if(i < lengths.size()){ return lengths[i]; } short ret = len; if(i < str_len){ if(i > 0){ len += font.getKerning(sf_str[i - 1], sf_str[i], style.pointSize); } len += font.getGlyph(sf_str[i], style.pointSize, false).advance; } // Memoize this function because it is destructive: lengths.push_back(ret); return ret; }; short i; for(i = 0; text_len(i) != text_len(i + 1) && i < str_len; i++) { unsigned short line_width = text_len(i) - text_len(last_line_break); if(((line_width > (dest_rect.width() - 6)) && (last_word_break >= last_line_break)) || (str[i] == '|' && !style.showPipes)) { if(str[i] == '|') { last_word_break = i + 1; } else if(last_line_break == last_word_break) last_word_break = i; break_info.push_back(std::make_tuple(last_line_break, last_word_break, line_width)); last_line_break = last_word_break; } if(str[i] == ' ') last_word_break = i + 1; } if(i - last_line_break > 0) { unsigned short line_width = text_len(i) - text_len(last_line_break); std::string snippet = str.substr(last_line_break); if(!snippet.empty()) break_info.push_back(std::make_tuple(last_line_break, str.length() + 1, line_width)); } return break_info; } // I don't know of a better way to do this than using pointers as keys. extern std::map> store_scale_aware_text; extern std::map store_clip_rects; void clear_scale_aware_text(sf::RenderTexture& texture) { store_scale_aware_text.erase(&texture); } // Offset the scale-aware text by the viewport top-left corner of the current // window mode: sf::Vector2f scaled_view_top_left(sf::RenderTarget& dest_window, sf::View& scaled_view) { sf::FloatRect viewport = scaled_view.getViewport(); rectangle windRect { dest_window }; sf::Vector2f top_left(viewport.left * windRect.width(), viewport.top * windRect.height()); return top_left; } void draw_scale_aware_text(sf::RenderTarget& dest_window, sf::Text str_to_draw) { if(dynamic_cast(&dest_window) != nullptr){ // Temporarily switch window to its unscaled view to draw scale-aware text sf::View scaled_view = dest_window.getView(); dest_window.setView(dest_window.getDefaultView()); sf::Vector2f text_position = str_to_draw.getPosition() * (float)get_ui_scale(); text_position += scaled_view_top_left(dest_window, scaled_view); // Rounding to whole-number positions *might* avoid unpredictable graphics bugs: round_vec(text_position); str_to_draw.setPosition(text_position); // Draw the text immediately dest_window.draw(str_to_draw); // Restore the scaled view dest_window.setView(scaled_view); }else if(dynamic_cast(&dest_window) != nullptr){ sf::RenderTexture* p = dynamic_cast(&dest_window); // Onto a RenderTexture is trickier because the texture is locked at the smaller size. // What we can do is save the sf::Text with its relative position and render // it onto the RenderWindow that eventually draws the RenderTexture. if(store_scale_aware_text.find(p) == store_scale_aware_text.end()){ store_scale_aware_text[p] = std::vector {}; } ScaleAwareText text = { str_to_draw, {}}; if(store_clip_rects.find(p) != store_clip_rects.end()){ text.clip_rect = store_clip_rects[p]; } store_scale_aware_text[p].push_back(text); } } std::string truncate_with_ellipsis(std::string str, const TextStyle& style, int width) { size_t length = string_length(str, style); if(length > width){ size_t ellipsis_length = string_length("...", style); size_t need_to_clip = length - width + ellipsis_length; int clip_idx = str.size() - 1; // clip_idx should never reach 0 for(; clip_idx >= 0; --clip_idx){ size_t clip_length = string_length(str.substr(clip_idx), style); if(clip_length >= need_to_clip) break; } str = str.substr(0, clip_idx) + "..."; } return str; } std::map substitutions = { {"–", "--"} }; static void win_draw_string(sf::RenderTarget& dest_window,rectangle dest_rect,std::string str,text_params_t& options) { if(str.empty()) return; // Nothing to do! short line_height = options.style.lineHeight; sf::Text str_to_draw; options.style.applyTo(str_to_draw); short adjust_x = 1, adjust_y = 10; str_to_draw.setString("fj"); // Something that has both an ascender and a descender // TODO: Why the heck are we drawing a whole line higher than requested!? adjust_y -= str_to_draw.getLocalBounds().height; for(auto it : substitutions){ boost::replace_all(str, it.first, it.second); } str_to_draw.setString(str); short total_width = str_to_draw.getLocalBounds().width; options.style.applyTo(str_to_draw, get_ui_scale()); eTextMode mode = options.mode; if(mode == eTextMode::WRAP && total_width < dest_rect.width() && !options.right_align) mode = eTextMode::LEFT_TOP; if(mode == eTextMode::LEFT_TOP && !options.style.showPipes && str.find('|') != std::string::npos) mode = eTextMode::WRAP; // Special stuff size_t iHilite = 0; location moveTo; line_height -= 2; // TODO: ...why are we arbitrarily reducing the line height from the requested value? // Pipes need to be left in the string that gets passed // to calculate_line_wrapping() or frame calculation is // broken. std::string str_with_pipes = str; if(!options.showBreaks && !options.style.showPipes){ for(int i=0; i < str.length(); ++i){ if(str[i] == '|') str[i] = ' '; } } if(mode == eTextMode::ELLIPSIS){ str = truncate_with_ellipsis(str, options.style, dest_rect.width()); mode = eTextMode::LEFT_TOP; } if(mode == eTextMode::WRAP){ break_info_t break_info = options.break_info; // It is better to pre-calculate line-wrapping and pass it in the options, // but if you don't, this will handle line-wrapping every frame: if(break_info.empty()){ break_info = calculate_line_wrapping(dest_rect, str_with_pipes, options.style); } moveTo = location(dest_rect.left + adjust_x, dest_rect.top + adjust_y); for(auto break_info_tuple : break_info){ if(options.right_align){ moveTo.x += (dest_rect.width() - std::get<2>(break_info_tuple)); } push_snippets(std::get<0>(break_info_tuple), std::get<1>(break_info_tuple), options, iHilite, str, moveTo); moveTo.y += line_height; moveTo.x = dest_rect.left; } } else { switch(mode) { // TODO: CENTRE and LEFT_BOTTOM don't work with text rect calculation... // Low priority, since the API doesn't even offer a way to do this. case eTextMode::CENTRE: // TODO: Calculate adjust_x/y directly instead... moveTo = location((dest_rect.right + dest_rect.left) / 2 - (4 * total_width) / 9, (dest_rect.bottom + dest_rect.top - line_height) / 2 + adjust_y - 1); adjust_x = moveTo.x - dest_rect.left; adjust_y = moveTo.y - dest_rect.top; break; case eTextMode::LEFT_TOP: moveTo = location(dest_rect.left + adjust_x, dest_rect.top + adjust_y); break; case eTextMode::LEFT_BOTTOM: moveTo = location(dest_rect.left + adjust_x, dest_rect.top + adjust_y + dest_rect.height() / 6); break; case eTextMode::WRAP: break; // Never happens, but put this here to silence warning } push_snippets(0, str.length() + 1, options, iHilite, str, moveTo); } for(auto& snippet : options.snippets) { str_to_draw.setString(snippet.text); str_to_draw.setPosition(snippet.at); if(snippet.hilited) { rectangle bounds = str_to_draw.getGlobalBounds(); // Width and height are too large by ui_scale because scale-aware text exists in unscaled space: bounds.width() /= get_ui_scale(); bounds.height() /= get_ui_scale(); // Adjust so that drawing the same text to // the same rect is positioned exactly right bounds.move_to(snippet.at); bounds.offset(-adjust_x, -adjust_y); if(options.returnType == text_params_t::RECTS) options.returnRects.push_back(bounds); str_to_draw.setColor(options.hilite_fg); bounds.inset(0,-4); fill_rect(dest_window, bounds, options.hilite_bg); } else str_to_draw.setColor(options.style.colour); draw_scale_aware_text(dest_window, str_to_draw); } } void win_draw_string(sf::RenderTarget& dest_window,rectangle dest_rect,std::string str,eTextMode mode,TextStyle style,bool right_align) { break_info_t break_info; win_draw_string(dest_window, dest_rect, str, mode, style, break_info, right_align); } void win_draw_string(sf::RenderTarget& dest_window,rectangle dest_rect,std::string str,eTextMode mode,TextStyle style,break_info_t break_info,bool right_align) { text_params_t params; params.mode = mode; params.style = style; params.break_info = break_info; params.right_align = right_align; win_draw_string(dest_window, dest_rect, str, params); } std::vector draw_string_hilite(sf::RenderTarget& dest_window,rectangle dest_rect,std::string str,TextStyle style,std::vector hilites,sf::Color hiliteClr) { text_params_t params; params.mode = eTextMode::WRAP; params.hilite_ranges = hilites; params.style = style; params.hilite_fg = hiliteClr; params.returnType = text_params_t::RECTS; win_draw_string(dest_window, dest_rect, str, params); return params.returnRects; } std::vector draw_string_sel(sf::RenderTarget& dest_window,rectangle dest_rect,std::string str,TextStyle style,std::vector hilites,sf::Color hiliteClr) { text_params_t params; params.mode = eTextMode::WRAP; params.showBreaks = true; params.hilite_ranges = hilites; params.style = style; params.hilite_fg = style.colour; params.hilite_bg = hiliteClr; params.returnType = text_params_t::RECTS; win_draw_string(dest_window, dest_rect, str, params); return params.snippets; } std::set strings_to_cache = {" ", "..."}; size_t string_length(std::string str, const TextStyle& style, short* height){ size_t total_width = 0; if(style.measurementCache.find(str) != style.measurementCache.end()){ location measurement = style.measurementCache[str]; if(height) *height = measurement.y; return measurement.x; } sf::Text text; style.applyTo(text); text.setString(str); total_width = text.getLocalBounds().width; if(strings_to_cache.count(str)){ location measurement; measurement.x = total_width; measurement.y = text.getLocalBounds().height; style.measurementCache[str] = measurement; } if(height) *height = text.getLocalBounds().height; return total_width; }