ui::label tagged text support: implement color tag

This commit is contained in:
Nikita Lisitsa 2022-05-21 21:45:23 +03:00
parent 7b99158b4d
commit 183f5283e2
2 changed files with 82 additions and 35 deletions

View file

@ -89,6 +89,7 @@ namespace psemek::ui
{
std::string_view text;
text_style style; // to be OR'd with base style
std::optional<gfx::color_rgba> color;
};
using chunk = std::variant<text_chunk>;
@ -107,6 +108,7 @@ namespace psemek::ui
struct batch
{
gfx::texture_2d const * texture;
gfx::color_rgba color;
std::vector<image> images;
};

View file

@ -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<std::string_view> const known_tags
{
"bold",
"uline",
"strike",
"color",
};
void check_attribute(std::string_view tag, std::optional<std::string_view> 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<std::string_view, int> tags_stack;
tags_stack["bold"] = 0;
tags_stack["uline"] = 0;
tags_stack["strike"] = 0;
std::unordered_map<std::string_view, std::vector<std::optional<std::string_view>>> tags_stack;
for (auto const & token : parse_result.tokens)
{
if (auto text = std::get_if<std::string_view>(&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<tagged_text::opening_tag>(&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<tagged_text::closing_tag>(&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<float>(*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<glyph> 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<geom::interval<float>> current_underline;
std::optional<geom::interval<float>> current_strikethrough;
std::optional<std::pair<geom::interval<float>, gfx::color_rgba>> current_underline;
std::optional<std::pair<geom::interval<float>, 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)
{