// license:BSD-3-Clause // copyright-holders:Nicola Salmoria, Aaron Giles, Nathan Woods /********************************************************************* text.cpp Text functionality for MAME's crude user interface *********************************************************************/ #include "text.h" #include "rendfont.h" #include "render.h" namespace ui { /*************************************************************************** INLINE FUNCTIONS ***************************************************************************/ //------------------------------------------------- // is_space_character //------------------------------------------------- inline bool is_space_character(unicode_char ch) { return ch == ' '; } //------------------------------------------------- // is_breakable_char - is a given unicode // character a possible line break? //------------------------------------------------- inline bool is_breakable_char(unicode_char ch) { // regular spaces and hyphens are breakable if (is_space_character(ch) || ch == '-') return true; // In the following character sets, any character is breakable: // Hiragana (3040-309F) // Katakana (30A0-30FF) // Bopomofo (3100-312F) // Hangul Compatibility Jamo (3130-318F) // Kanbun (3190-319F) // Bopomofo Extended (31A0-31BF) // CJK Strokes (31C0-31EF) // Katakana Phonetic Extensions (31F0-31FF) // Enclosed CJK Letters and Months (3200-32FF) // CJK Compatibility (3300-33FF) // CJK Unified Ideographs Extension A (3400-4DBF) // Yijing Hexagram Symbols (4DC0-4DFF) // CJK Unified Ideographs (4E00-9FFF) if (ch >= 0x3040 && ch <= 0x9fff) return true; // Hangul Syllables (AC00-D7AF) are breakable if (ch >= 0xac00 && ch <= 0xd7af) return true; // CJK Compatibility Ideographs (F900-FAFF) are breakable if (ch >= 0xf900 && ch <= 0xfaff) return true; return false; } /*************************************************************************** CORE IMPLEMENTATION ***************************************************************************/ //------------------------------------------------- // ctor //------------------------------------------------- text_layout::text_layout(render_font &font, float xscale, float yscale, float width, text_layout::text_justify justify, text_layout::word_wrapping wrap) : m_font(font), m_xscale(xscale), m_yscale(yscale), m_width(width), m_justify(justify), m_wrap(wrap), m_current_line(nullptr), m_last_break(0), m_text_position(0), m_truncating(false) { invalidate_calculated_actual_width(); } //------------------------------------------------- // ctor (move) //------------------------------------------------- text_layout::text_layout(text_layout &&that) : m_font(that.m_font), m_xscale(that.m_xscale), m_yscale(that.m_yscale), m_width(that.m_width), m_calculated_actual_width(that.m_calculated_actual_width), m_justify(that.m_justify), m_wrap(that.m_wrap), m_lines(std::move(that.m_lines)), m_current_line(that.m_current_line), m_last_break(that.m_last_break), m_text_position(that.m_text_position), m_truncating(false) { that.invalidate_calculated_actual_width(); } //------------------------------------------------- // dtor //------------------------------------------------- text_layout::~text_layout() { } //------------------------------------------------- // add_text //------------------------------------------------- void text_layout::add_text(const char *text, const char_style &style) { int position = 0; int text_length = strlen(text); while(position < text_length) { // adding a character - we might change the width invalidate_calculated_actual_width(); // do we need to create a new line? if (m_current_line == nullptr) { // get the current character unicode_char schar; int scharcount; scharcount = uchar_from_utf8(&schar, &text[position], text_length - position); if (scharcount == -1) break; // if the line starts with a tab character, center it regardless text_justify line_justify = justify(); if (schar == '\t') { position += scharcount; line_justify = text_layout::CENTER; } // start a new line start_new_line(line_justify, style.size); } // get the current character int scharcount; unicode_char ch; scharcount = uchar_from_utf8(&ch, &text[position], text_length - position); if (scharcount < 0) break; position += scharcount; // set up source information source_info source = { 0, }; source.start = m_text_position; source.span = scharcount; m_text_position += scharcount; // is this an endline? if (ch == '\n') { // first, start a line if we have not already if (m_current_line == nullptr) start_new_line(LEFT, style.size); // and then close up the current line m_current_line = nullptr; } else if (!m_truncating) { // if we hit a space, remember the location and width *without* the space if (is_space_character(ch)) m_last_break = m_current_line->character_count(); // append the character m_current_line->add_character(ch, style, source); // do we have to wrap? if (wrap() != NEVER && m_current_line->width() > m_width) { switch (wrap()) { case TRUNCATE: truncate_wrap(); break; case WORD: word_wrap(); break; default: fatalerror("invalid word wrapping value"); break; } } else { // we didn't wrap - if we hit any non-space breakable character, remember the location and width // *with* the breakable character if (ch != ' ' && is_breakable_char(ch)) m_last_break = m_current_line->character_count(); } } } } //------------------------------------------------- // invalidate_calculated_actual_width //------------------------------------------------- void text_layout::invalidate_calculated_actual_width() { m_calculated_actual_width = -1; } //------------------------------------------------- // actual_left //------------------------------------------------- float text_layout::actual_left() const { float result; if (empty()) { // degenerate scenario result = 0; } else { result = 1.0f; for (const auto &line : m_lines) { result = std::min(result, line->xoffset()); // take an opportunity to break out easily if (result <= 0) break; } } return result; } //------------------------------------------------- // actual_width //------------------------------------------------- float text_layout::actual_width() const { // do we need to calculate the width? if (m_calculated_actual_width < 0) { // calculate the actual width m_calculated_actual_width = 0; for (const auto &line : m_lines) m_calculated_actual_width = std::max(m_calculated_actual_width, line->width()); } // return it return m_calculated_actual_width; } //------------------------------------------------- // actual_height //------------------------------------------------- float text_layout::actual_height() const { line *last_line = (m_lines.size() > 0) ? m_lines[m_lines.size() - 1].get() : nullptr; return last_line ? last_line->yoffset() + last_line->height() : 0; } //------------------------------------------------- // start_new_line //------------------------------------------------- void text_layout::start_new_line(text_layout::text_justify justify, float height) { // create a new line std::unique_ptr new_line(global_alloc_clear(*this, justify, actual_height(), height * yscale())); // update the current line m_current_line = new_line.get(); m_last_break = 0; m_truncating = false; // append it m_lines.push_back(std::move(new_line)); } //------------------------------------------------- // get_char_width //------------------------------------------------- float text_layout::get_char_width(unicode_char ch, float size) { return font().char_width(size * yscale(), xscale() / yscale(), ch); } //------------------------------------------------- // truncate_wrap //------------------------------------------------- void text_layout::truncate_wrap() { const unicode_char elipsis = 0x2026; // for now, lets assume that we're only truncating the last character size_t truncate_position = m_current_line->character_count() - 1; const auto& truncate_char = m_current_line->character(truncate_position); // copy style information char_style style = truncate_char.style; // copy source information source_info source = { 0, }; source.start = truncate_char.source.start + truncate_char.source.span; source.span = 0; // figure out how wide an elipsis is float elipsis_width = get_char_width(elipsis, style.size); // where should we really truncate from? while (truncate_position > 0 && m_current_line->character(truncate_position).xoffset + elipsis_width > width()) truncate_position--; // truncate!!! m_current_line->truncate(truncate_position); // and append the elipsis m_current_line->add_character(elipsis, style, source); // take note that we are truncating; supress new characters m_truncating = true; } //------------------------------------------------- // word_wrap //------------------------------------------------- void text_layout::word_wrap() { // keep track of the last line and break line *last_line = m_current_line; size_t last_break = m_last_break; // start a new line with the same justification start_new_line(last_line->justify(), last_line->character(last_line->character_count() - 1).style.size); // find the begining of the word to wrap size_t position = last_break; while (position + 1 < last_line->character_count() && is_space_character(last_line->character(position).character)) position++; // transcribe the characters for (size_t i = position; i < last_line->character_count(); i++) { auto &ch = last_line->character(i); m_current_line->add_character(ch.character, ch.style, ch.source); } // and finally, truncate the last line last_line->truncate(last_break); } //------------------------------------------------- // hit_test //------------------------------------------------- bool text_layout::hit_test(float x, float y, size_t &start, size_t &span) const { for (const auto &line : m_lines) { if (y >= line->yoffset() && y < line->yoffset() + line->height()) { float line_xoffset = line->xoffset(); if (x >= line_xoffset && x < line_xoffset + line->width()) { for (size_t i = 0; i < line->character_count(); i++) { const auto &ch = line->character(i); if (x >= ch.xoffset && x < ch.xoffset + ch.xwidth) { start = ch.source.start; span = ch.source.span; return true; } } } } } start = 0; span = 0; return false; } //------------------------------------------------- // restyle //------------------------------------------------- void text_layout::restyle(size_t start, size_t span, rgb_t *fgcolor, rgb_t *bgcolor) { for (const auto &line : m_lines) { for (size_t i = 0; i < line->character_count(); i++) { auto &ch = line->character(i); if (ch.source.start >= start && ch.source.start + ch.source.span <= start + span) { if (fgcolor != nullptr) ch.style.fgcolor = *fgcolor; if (bgcolor != nullptr) ch.style.bgcolor = *bgcolor; } } } } //------------------------------------------------- // get_wrap_info //------------------------------------------------- int text_layout::get_wrap_info(std::vector &xstart, std::vector &xend) const { // this is a hacky method (tailored to the need to implement // mame_ui_manager::wrap_text) but so be it int line_count = 0; for (const auto &line : m_lines) { int start_pos = 0; int end_pos = 0; auto line_character_count = line->character_count(); if (line_character_count > 0) { start_pos = line->character(0).source.start; end_pos = line->character(line_character_count - 1).source.start + line->character(line_character_count - 1).source.span; } line_count++; xstart.push_back(start_pos); xend.push_back(end_pos); } return line_count; } //------------------------------------------------- // emit //------------------------------------------------- void text_layout::emit(render_container *container, float x, float y) { for (const auto &line : m_lines) { float line_xoffset = line->xoffset(); // emit every single character for (auto i = 0; i < line->character_count(); i++) { auto &ch = line->character(i); // position this specific character correctly (TODO - this doesn't // handle differently sized text (yet) float char_x = x + line_xoffset + ch.xoffset; float char_y = y + line->yoffset(); float char_width = ch.xwidth; float char_height = line->height(); // render the background of the character (if present) if (ch.style.bgcolor.a() != 0) container->add_rect(char_x, char_y, char_x + char_width, char_y + char_height, ch.style.bgcolor, PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA)); // render the foreground container->add_char( char_x, char_y, char_height, xscale() / yscale(), ch.style.fgcolor, font(), ch.character); } } } //------------------------------------------------- // line::ctor //------------------------------------------------- text_layout::line::line(text_layout &layout, text_justify justify, float yoffset, float height) : m_layout(layout), m_justify(justify), m_yoffset(yoffset), m_width(0.0), m_height(height) { } //------------------------------------------------- // line::add_character //------------------------------------------------- void text_layout::line::add_character(unicode_char ch, const char_style &style, const source_info &source) { // get the width of this character float chwidth = m_layout.get_char_width(ch, style.size); // create the positioned character positioned_char positioned_char = { 0, }; positioned_char.character = ch; positioned_char.xoffset = m_width; positioned_char.xwidth = chwidth; positioned_char.style = style; positioned_char.source = source; // append the character m_characters.push_back(positioned_char); m_width += chwidth; // we might be bigger m_height = std::max(m_height, style.size * m_layout.yscale()); } //------------------------------------------------- // line::xoffset //------------------------------------------------- float text_layout::line::xoffset() const { float result; switch (justify()) { case LEFT: default: result = 0; break; case CENTER: result = (m_layout.width() - width()) / 2; break; case RIGHT: result = m_layout.width() - width(); break; } return result; } //------------------------------------------------- // line::truncate //------------------------------------------------- void text_layout::line::truncate(size_t position) { assert(position <= m_characters.size()); // are we actually truncating? if (position < m_characters.size()) { // set the width as appropriate m_width = m_characters[position].xoffset; // and resize the array m_characters.resize(position); } } } // namespace ui