From 9300dc677936498e409f3cedc4053631a7ef2680 Mon Sep 17 00:00:00 2001 From: lisyarus Date: Sun, 22 May 2022 11:16:01 +0300 Subject: [PATCH] Support color tag in ui::label --- libs/ui/include/psemek/ui/label.hpp | 9 +- libs/ui/source/label.cpp | 172 +++++++++++++++++++--------- 2 files changed, 126 insertions(+), 55 deletions(-) diff --git a/libs/ui/include/psemek/ui/label.hpp b/libs/ui/include/psemek/ui/label.hpp index 729f0c86..bcdfa2a8 100644 --- a/libs/ui/include/psemek/ui/label.hpp +++ b/libs/ui/include/psemek/ui/label.hpp @@ -98,7 +98,13 @@ namespace psemek::ui std::optional color; }; - using chunk = std::variant; + struct image_chunk + { + std::string_view id; + std::optional color; + }; + + using chunk = std::variant; std::string text_; std::vector chunks_; @@ -115,6 +121,7 @@ namespace psemek::ui { gfx::texture_2d const * texture; gfx::color_rgba color; + bool text; std::vector images; }; diff --git a/libs/ui/source/label.cpp b/libs/ui/source/label.cpp index d8b51de0..4087a15e 100644 --- a/libs/ui/source/label.cpp +++ b/libs/ui/source/label.cpp @@ -74,7 +74,17 @@ namespace psemek::ui } else if (auto tag = std::get_if(&token)) { - if (known_tags.contains(tag->type)) + if (tag->type == "image") + { + if (!tag->attribute) + throw std::runtime_error("tag [image] requires an attribute"); + image_chunk chunk; + chunk.id = *tag->attribute; + if (!tags_stack["color"].empty()) + chunk.color = *gfx::parse_color(*tags_stack["color"].back()); + chunks_.push_back(chunk); + } + else if (known_tags.contains(tag->type)) { check_attribute(tag->type, tag->attribute); tags_stack[tag->type].push_back(tag->attribute); @@ -85,7 +95,7 @@ namespace psemek::ui else if (auto tag = std::get_if(&token)) { if (!known_tags.contains(tag->type)) - throw std::runtime_error("unknown tag [" + std::string(tag->type) + "]"); + throw std::runtime_error("unknown closing tag [" + std::string(tag->type) + "]"); if (tags_stack[tag->type].empty()) throw std::runtime_error("mismatched opening & closing tags for [" + std::string(tag->type) + "]"); tags_stack[tag->type].pop_back(); @@ -191,6 +201,7 @@ namespace psemek::ui auto const offset = geom::cast(*st->text_shadow_offset); for (auto const & batch : cached_state_->batches) { + if (!batch.text) continue; if (batch.color[3] < 255) continue; for (auto const & image : batch.images) p.draw_image(image.position + offset, gfx::texture_view_2d{batch.texture, image.texcoords}, {*st->shadow_color, painter::color_mode::multiply}); @@ -199,7 +210,7 @@ namespace psemek::ui for (auto const & batch : cached_state_->batches) for (auto const & image : batch.images) - p.draw_image(image.position, gfx::texture_view_2d{batch.texture, image.texcoords}, {batch.color, painter::color_mode::multiply}); + p.draw_image(image.position, gfx::texture_view_2d{batch.texture, image.texcoords}, {batch.color, batch.text ? painter::color_mode::multiply : painter::color_mode::mix}); } void label::on_state_changed() @@ -229,19 +240,26 @@ namespace psemek::ui auto const font_height = st->font->size()[1] * (*st->text_scale); - // Shape glyphs chunk by chunk - // All glyphs within a chunk have - // the exact same font properties + // Shape items (glyphs/images) chunk by chunk + // All items within a chunk have + // the exact same properties - struct glyph_chunk + struct item + { + geom::box position; + std::optional character; + }; + + struct item_chunk { std::size_t end; text_style style; gfx::color_rgba color; + gfx::texture_view_2d image; }; - std::vector glyphs; - std::vector glyph_chunks; + std::vector items; + std::vector item_chunks; { geom::point pen{0.f, 0.f}; @@ -258,19 +276,42 @@ namespace psemek::ui auto font = bold ? st->bold_font.get() : st->font.get(); - auto chunk_glyphs = font->shape(text->text, opts, pen); - auto color = text->color ? *text->color : *st->text_color; - glyphs.reserve(glyphs.size() + chunk_glyphs.size()); - glyphs.insert(glyphs.end(), chunk_glyphs.begin(), chunk_glyphs.end()); + auto glyphs = font->shape(text->text, opts, pen); - glyph_chunks.push_back({glyphs.size(), merged_text_style, color}); + items.reserve(items.size() + glyphs.size()); + for (auto const & g : glyphs) + items.push_back(item{g.position, g.character}); + + item_chunks.push_back({items.size(), merged_text_style, color, {}}); + } + else if (auto image = std::get_if(&chunk)) + { + auto color = image->color ? *image->color : gfx::color_rgba{255, 255, 255, 0}; + + if (!image_provider_) + throw std::runtime_error("cannot use [image] tag without image provider"); + + auto texture = image_provider_->get(image->id); + + float const scale = *st->text_scale; + + geom::box position; + position[0].min = pen[0]; + position[0].max = pen[0] + texture.width() * scale; + position[1].min = pen[1] + font_height / 2.f - texture.height() * scale / 2.f; + position[1].max = position[1].min + texture.height() * scale; + + pen[0] = position[0].max; + + items.push_back({position, {}}); + item_chunks.push_back({items.size(), {}, color, texture}); } } } - // Break glyphs into lines + // Break items into lines struct line_range { @@ -281,30 +322,30 @@ namespace psemek::ui std::vector lines; { - std::size_t current_glyph = 0; - while (current_glyph < glyphs.size()) + std::size_t current_item = 0; + while (current_item < items.size()) { if (skip_spaces_) { - while (current_glyph < glyphs.size() && std::isspace(glyphs[current_glyph].character)) - ++current_glyph; + while (current_item < items.size() && items[current_item].character && std::isspace(*items[current_item].character)) + ++current_item; } - std::size_t line_begin = current_glyph; + std::size_t line_begin = current_item; bool newline_end = false; bool wrap_end = false; geom::interval x_range; - while (current_glyph < glyphs.size()) + while (current_item < items.size()) { - if (is_newline(glyphs[current_glyph].character)) + if (items[current_item].character && is_newline(*items[current_item].character)) { newline_end = true; break; } - x_range |= glyphs[current_glyph].position[0]; + x_range |= items[current_item].position[0]; if (x_range.length() > bbox[0].length() && wrap_) { @@ -312,26 +353,26 @@ namespace psemek::ui break; } - ++current_glyph; + ++current_item; } - if (wrap_end && current_glyph == line_begin) + if (wrap_end && current_item == line_begin) break; - if (wrap_end && current_glyph > line_begin) + if (wrap_end && current_item > line_begin) { - std::size_t space_pos = current_glyph - 1; - while (space_pos > line_begin && !std::isspace(glyphs[space_pos].character)) + std::size_t space_pos = current_item - 1; + while (space_pos > line_begin && (!items[current_item].character || !std::isspace(*items[space_pos].character))) --space_pos; if (space_pos > line_begin) - current_glyph = space_pos; + current_item = space_pos; } - lines.push_back({line_begin, current_glyph}); + lines.push_back({line_begin, current_item}); if (newline_end) - ++current_glyph; + ++current_item; } } @@ -345,7 +386,7 @@ namespace psemek::ui // hardcode font height as min line height bbox[1] = {0.f, 1.f * font_height}; for (std::size_t i = lines[l].begin; i < lines[l].end; ++i) - bbox |= glyphs[i].position; + bbox |= items[i].position; line_bbox[l] = bbox; } @@ -360,7 +401,7 @@ namespace psemek::ui // TODO: handle text overflow - // Position lines & glyphs inside label bbox + // Position lines & items inside label bbox std::vector> line_offset(lines.size()); { @@ -383,7 +424,7 @@ namespace psemek::ui int spaces = 0; for (std::size_t i = lines[l].begin; i < lines[l].end; ++i) - if (std::isspace(glyphs[i].character)) + if (items[i].character && std::isspace(*items[i].character)) ++spaces; geom::vector offset{0.f, current_y}; @@ -411,8 +452,8 @@ namespace psemek::ui for (std::size_t i = lines[l].begin; i < lines[l].end; ++i) { - glyphs[i].position += offset; - if (std::isspace(glyphs[i].character)) + items[i].position += offset; + if (items[i].character && std::isspace(*items[i].character)) offset[0] += space_extra; } @@ -424,27 +465,49 @@ namespace psemek::ui { std::size_t begin = 0; - for (auto const & ch : glyph_chunks) + for (auto const & ch : item_chunks) { auto & batch = state.batches.emplace_back(); - auto font = ch.style.is_set(text_style_flag::bold) ? st->bold_font.get() : st->font.get(); - - batch.texture = &font->atlas(); - batch.color = ch.color; - - for (std::size_t i = begin; i < ch.end; ++i) + if (ch.image) { - auto & g = glyphs[i]; - auto tc = font->texcoords(g.character); - if (!tc) continue; + batch.texture = ch.image.texture; + batch.color = ch.color; + batch.text = false; - g.position[0] += (std::round(g.position[0].min) - g.position[0].min); - g.position[1] += (std::round(g.position[1].min) - g.position[1].min); + for (std::size_t i = begin; i < ch.end; ++i) + { + auto & g = items[i]; - auto & image = batch.images.emplace_back(); - image.position = g.position; - image.texcoords = *tc; + g.position[0] += (std::round(g.position[0].min) - g.position[0].min); + g.position[1] += (std::round(g.position[1].min) - g.position[1].min); + + auto & image = batch.images.emplace_back(); + image.position = g.position; + image.texcoords = ch.image.part; + } + } + else + { + auto font = ch.style.is_set(text_style_flag::bold) ? st->bold_font.get() : st->font.get(); + + batch.texture = &font->atlas(); + batch.color = ch.color; + batch.text = true; + + for (std::size_t i = begin; i < ch.end; ++i) + { + auto & g = items[i]; + auto tc = font->texcoords(*g.character); + if (!tc) continue; + + g.position[0] += (std::round(g.position[0].min) - g.position[0].min); + g.position[1] += (std::round(g.position[1].min) - g.position[1].min); + + auto & image = batch.images.emplace_back(); + image.position = g.position; + image.texcoords = *tc; + } } begin = ch.end; @@ -463,6 +526,7 @@ namespace psemek::ui auto & b = state.batches.emplace_back(); b.texture = texture; b.color = color; + b.text = true; } return state.batches.back(); }; @@ -477,9 +541,9 @@ namespace psemek::ui std::optional last_underline_end; std::optional last_strikethrough_end; - for (; ch < glyph_chunks.size(); ++ch) + for (; ch < item_chunks.size(); ++ch) { - auto const & chunk = glyph_chunks[ch]; + auto const & chunk = item_chunks[ch]; bool const underline = chunk.style.is_set(text_style_flag::underline); bool const strikethrough = chunk.style.is_set(text_style_flag::strikethrough); @@ -491,7 +555,7 @@ namespace psemek::ui { geom::interval x_range; for (std::size_t i = ibegin; i < iend; ++i) - x_range |= glyphs[i].position[0]; + x_range |= items[i].position[0]; if (underline) {