From 183f5283e26218f203c18314fdbdf45b17c9d794 Mon Sep 17 00:00:00 2001 From: lisyarus Date: Sat, 21 May 2022 21:45:23 +0300 Subject: [PATCH] ui::label tagged text support: implement color tag --- libs/ui/include/psemek/ui/label.hpp | 2 + libs/ui/source/label.cpp | 115 +++++++++++++++++++--------- 2 files changed, 82 insertions(+), 35 deletions(-) diff --git a/libs/ui/include/psemek/ui/label.hpp b/libs/ui/include/psemek/ui/label.hpp index 6eafc9b7..97edd4a9 100644 --- a/libs/ui/include/psemek/ui/label.hpp +++ b/libs/ui/include/psemek/ui/label.hpp @@ -89,6 +89,7 @@ namespace psemek::ui { std::string_view text; text_style style; // to be OR'd with base style + std::optional color; }; using chunk = std::variant; @@ -107,6 +108,7 @@ namespace psemek::ui struct batch { gfx::texture_2d const * texture; + gfx::color_rgba color; std::vector images; }; diff --git a/libs/ui/source/label.cpp b/libs/ui/source/label.cpp index a82bc86a..1f48d077 100644 --- a/libs/ui/source/label.cpp +++ b/libs/ui/source/label.cpp @@ -17,11 +17,35 @@ namespace psemek::ui text_ = std::move(text); chunks_.clear(); - chunks_.push_back(text_chunk{{text_.data(), text_.size()}, text_style{}}); + chunks_.push_back(text_chunk{{text_.data(), text_.size()}, text_style{}, std::nullopt}); on_state_changed(); } + static std::unordered_set const known_tags + { + "bold", + "uline", + "strike", + "color", + }; + + void check_attribute(std::string_view tag, std::optional const & attribute) + { + if (tag == "bold" || tag == "uline" || tag == "strike") + { + if (attribute) + throw std::runtime_error("tag [" + std::string(tag) + " doesn't support attributes"); + } + else if (tag == "color") + { + if (!attribute) + throw std::runtime_error("tag [color] requires an attribute"); + if (!gfx::parse_color(*attribute)) + throw std::runtime_error("failed to parse color \"" + std::string(*attribute) + "\""); + } + } + void label::set_tagged_text(std::string text) { text_ = std::move(text); @@ -30,47 +54,41 @@ namespace psemek::ui auto parse_result = tagged_text::parse(text_); - std::unordered_map tags_stack; - tags_stack["bold"] = 0; - tags_stack["uline"] = 0; - tags_stack["strike"] = 0; + std::unordered_map>> tags_stack; for (auto const & token : parse_result.tokens) { if (auto text = std::get_if(&token)) { text_chunk chunk; - if (tags_stack["bold"] > 0) + if (!tags_stack["bold"].empty()) chunk.style.set(text_style_flag::bold); - if (tags_stack["uline"] > 0) + if (!tags_stack["uline"].empty()) chunk.style.set(text_style_flag::underline); - if (tags_stack["strike"] > 0) + if (!tags_stack["strike"].empty()) chunk.style.set(text_style_flag::strikethrough); + if (!tags_stack["color"].empty()) + chunk.color = *gfx::parse_color(*tags_stack["color"].back()); chunk.text = *text; chunks_.push_back(chunk); } else if (auto tag = std::get_if(&token)) { - if (tag->type == "bold") + if (known_tags.contains(tag->type)) { - tags_stack["bold"] += 1; - } - else if (tag->type == "uline") - { - tags_stack["uline"] += 1; - } - else if (tag->type == "strike") - { - tags_stack["strike"] += 1; + check_attribute(tag->type, tag->attribute); + tags_stack[tag->type].push_back(tag->attribute); } else throw std::runtime_error("unknown tag [" + std::string(tag->type) + "]"); } else if (auto tag = std::get_if(&token)) { - if (tags_stack[tag->type] == 0) + if (!known_tags.contains(tag->type)) + throw std::runtime_error("unknown 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] -= 1; + tags_stack[tag->type].pop_back(); } } @@ -165,13 +183,16 @@ namespace psemek::ui { auto const offset = geom::cast(*st->text_shadow_offset); for (auto const & batch : cached_state_->batches) + { + 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}); + } } 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}, {*st->text_color, painter::color_mode::multiply}); + p.draw_image(image.position, gfx::texture_view_2d{batch.texture, image.texcoords}, {batch.color, painter::color_mode::multiply}); } void label::on_state_changed() @@ -209,6 +230,7 @@ namespace psemek::ui { std::size_t end; text_style style; + gfx::color_rgba color; }; std::vector glyphs; @@ -231,10 +253,12 @@ namespace psemek::ui 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()); - glyph_chunks.push_back({glyphs.size(), merged_text_style}); + glyph_chunks.push_back({glyphs.size(), merged_text_style, color}); } } } @@ -400,6 +424,7 @@ namespace psemek::ui 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) { @@ -424,9 +449,6 @@ namespace psemek::ui { float const line_width = 1.f * (*st->text_scale); - auto & batch = state.batches.emplace_back(); - batch.texture = single_white_pixel_texture().get(); - std::size_t ch_begin = 0; std::size_t ch = 0; for (std::size_t l = 0; l < lines.size(); ++l) @@ -434,19 +456,30 @@ namespace psemek::ui float underline_y = std::round(line_offset[l][1] + font_height - line_width); float strikethrough_y = std::round(line_offset[l][1] + font_height / 2.f - line_width / 2.f); - std::optional> current_underline; - std::optional> current_strikethrough; + std::optional, gfx::color_rgba>> current_underline; + std::optional, gfx::color_rgba>> current_strikethrough; + + auto batch = [&](gfx::color_rgba const & color) -> cached_state::batch & { + auto texture = single_white_pixel_texture().get(); + if (state.batches.empty() || state.batches.back().texture != texture || state.batches.back().color != color) + { + auto & b = state.batches.emplace_back(); + b.texture = texture; + b.color = color; + } + return state.batches.back(); + }; auto flush_underline = [&]{ - auto & image = batch.images.emplace_back(); - image.position[0] = *current_underline; + auto & image = batch(current_underline->second).images.emplace_back(); + image.position[0] = current_underline->first; image.position[1] = {underline_y, underline_y + line_width}; current_underline = std::nullopt; }; auto flush_strikethrough = [&]{ - auto & image = batch.images.emplace_back(); - image.position[0] = *current_strikethrough; + auto & image = batch(current_strikethrough->second).images.emplace_back(); + image.position[0] = current_strikethrough->first; image.position[1] = {strikethrough_y, strikethrough_y + line_width}; current_strikethrough = std::nullopt; }; @@ -469,11 +502,17 @@ namespace psemek::ui if (underline && current_underline) { - *current_underline |= x_range; + if (current_underline->second != chunk.color) + { + flush_underline(); + current_underline = {x_range, chunk.color}; + } + else + current_underline->first |= x_range; } else if (underline && !current_underline) { - current_underline = x_range; + current_underline = {x_range, chunk.color}; } else if (!underline && current_underline) { @@ -482,11 +521,17 @@ namespace psemek::ui if (strikethrough && current_strikethrough) { - *current_strikethrough |= x_range; + if (current_strikethrough->second != chunk.color) + { + flush_strikethrough(); + current_strikethrough = {x_range, chunk.color}; + } + else + current_strikethrough->first |= x_range; } else if (strikethrough && !current_strikethrough) { - current_strikethrough = x_range; + current_strikethrough = {x_range, chunk.color}; } else if (!strikethrough && current_strikethrough) {