ui::label tagged text support: implement color tag
This commit is contained in:
parent
7b99158b4d
commit
183f5283e2
2 changed files with 82 additions and 35 deletions
|
|
@ -89,6 +89,7 @@ namespace psemek::ui
|
||||||
{
|
{
|
||||||
std::string_view text;
|
std::string_view text;
|
||||||
text_style style; // to be OR'd with base style
|
text_style style; // to be OR'd with base style
|
||||||
|
std::optional<gfx::color_rgba> color;
|
||||||
};
|
};
|
||||||
|
|
||||||
using chunk = std::variant<text_chunk>;
|
using chunk = std::variant<text_chunk>;
|
||||||
|
|
@ -107,6 +108,7 @@ namespace psemek::ui
|
||||||
struct batch
|
struct batch
|
||||||
{
|
{
|
||||||
gfx::texture_2d const * texture;
|
gfx::texture_2d const * texture;
|
||||||
|
gfx::color_rgba color;
|
||||||
std::vector<image> images;
|
std::vector<image> images;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,35 @@ namespace psemek::ui
|
||||||
text_ = std::move(text);
|
text_ = std::move(text);
|
||||||
|
|
||||||
chunks_.clear();
|
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();
|
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)
|
void label::set_tagged_text(std::string text)
|
||||||
{
|
{
|
||||||
text_ = std::move(text);
|
text_ = std::move(text);
|
||||||
|
|
@ -30,47 +54,41 @@ namespace psemek::ui
|
||||||
|
|
||||||
auto parse_result = tagged_text::parse(text_);
|
auto parse_result = tagged_text::parse(text_);
|
||||||
|
|
||||||
std::unordered_map<std::string_view, int> tags_stack;
|
std::unordered_map<std::string_view, std::vector<std::optional<std::string_view>>> tags_stack;
|
||||||
tags_stack["bold"] = 0;
|
|
||||||
tags_stack["uline"] = 0;
|
|
||||||
tags_stack["strike"] = 0;
|
|
||||||
|
|
||||||
for (auto const & token : parse_result.tokens)
|
for (auto const & token : parse_result.tokens)
|
||||||
{
|
{
|
||||||
if (auto text = std::get_if<std::string_view>(&token))
|
if (auto text = std::get_if<std::string_view>(&token))
|
||||||
{
|
{
|
||||||
text_chunk chunk;
|
text_chunk chunk;
|
||||||
if (tags_stack["bold"] > 0)
|
if (!tags_stack["bold"].empty())
|
||||||
chunk.style.set(text_style_flag::bold);
|
chunk.style.set(text_style_flag::bold);
|
||||||
if (tags_stack["uline"] > 0)
|
if (!tags_stack["uline"].empty())
|
||||||
chunk.style.set(text_style_flag::underline);
|
chunk.style.set(text_style_flag::underline);
|
||||||
if (tags_stack["strike"] > 0)
|
if (!tags_stack["strike"].empty())
|
||||||
chunk.style.set(text_style_flag::strikethrough);
|
chunk.style.set(text_style_flag::strikethrough);
|
||||||
|
if (!tags_stack["color"].empty())
|
||||||
|
chunk.color = *gfx::parse_color(*tags_stack["color"].back());
|
||||||
chunk.text = *text;
|
chunk.text = *text;
|
||||||
chunks_.push_back(chunk);
|
chunks_.push_back(chunk);
|
||||||
}
|
}
|
||||||
else if (auto tag = std::get_if<tagged_text::opening_tag>(&token))
|
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;
|
check_attribute(tag->type, tag->attribute);
|
||||||
}
|
tags_stack[tag->type].push_back(tag->attribute);
|
||||||
else if (tag->type == "uline")
|
|
||||||
{
|
|
||||||
tags_stack["uline"] += 1;
|
|
||||||
}
|
|
||||||
else if (tag->type == "strike")
|
|
||||||
{
|
|
||||||
tags_stack["strike"] += 1;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw std::runtime_error("unknown tag [" + std::string(tag->type) + "]");
|
throw std::runtime_error("unknown tag [" + std::string(tag->type) + "]");
|
||||||
}
|
}
|
||||||
else if (auto tag = std::get_if<tagged_text::closing_tag>(&token))
|
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) + "]");
|
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);
|
auto const offset = geom::cast<float>(*st->text_shadow_offset);
|
||||||
for (auto const & batch : cached_state_->batches)
|
for (auto const & batch : cached_state_->batches)
|
||||||
|
{
|
||||||
|
if (batch.color[3] < 255) continue;
|
||||||
for (auto const & image : batch.images)
|
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});
|
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 & batch : cached_state_->batches)
|
||||||
for (auto const & image : batch.images)
|
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()
|
void label::on_state_changed()
|
||||||
|
|
@ -209,6 +230,7 @@ namespace psemek::ui
|
||||||
{
|
{
|
||||||
std::size_t end;
|
std::size_t end;
|
||||||
text_style style;
|
text_style style;
|
||||||
|
gfx::color_rgba color;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<glyph> glyphs;
|
std::vector<glyph> glyphs;
|
||||||
|
|
@ -231,10 +253,12 @@ namespace psemek::ui
|
||||||
|
|
||||||
auto chunk_glyphs = font->shape(text->text, opts, pen);
|
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.reserve(glyphs.size() + chunk_glyphs.size());
|
||||||
glyphs.insert(glyphs.end(), chunk_glyphs.begin(), chunk_glyphs.end());
|
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();
|
auto font = ch.style.is_set(text_style_flag::bold) ? st->bold_font.get() : st->font.get();
|
||||||
|
|
||||||
batch.texture = &font->atlas();
|
batch.texture = &font->atlas();
|
||||||
|
batch.color = ch.color;
|
||||||
|
|
||||||
for (std::size_t i = begin; i < ch.end; ++i)
|
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);
|
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_begin = 0;
|
||||||
std::size_t ch = 0;
|
std::size_t ch = 0;
|
||||||
for (std::size_t l = 0; l < lines.size(); ++l)
|
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 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);
|
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<std::pair<geom::interval<float>, gfx::color_rgba>> current_underline;
|
||||||
std::optional<geom::interval<float>> current_strikethrough;
|
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 flush_underline = [&]{
|
||||||
auto & image = batch.images.emplace_back();
|
auto & image = batch(current_underline->second).images.emplace_back();
|
||||||
image.position[0] = *current_underline;
|
image.position[0] = current_underline->first;
|
||||||
image.position[1] = {underline_y, underline_y + line_width};
|
image.position[1] = {underline_y, underline_y + line_width};
|
||||||
current_underline = std::nullopt;
|
current_underline = std::nullopt;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto flush_strikethrough = [&]{
|
auto flush_strikethrough = [&]{
|
||||||
auto & image = batch.images.emplace_back();
|
auto & image = batch(current_strikethrough->second).images.emplace_back();
|
||||||
image.position[0] = *current_strikethrough;
|
image.position[0] = current_strikethrough->first;
|
||||||
image.position[1] = {strikethrough_y, strikethrough_y + line_width};
|
image.position[1] = {strikethrough_y, strikethrough_y + line_width};
|
||||||
current_strikethrough = std::nullopt;
|
current_strikethrough = std::nullopt;
|
||||||
};
|
};
|
||||||
|
|
@ -469,11 +502,17 @@ namespace psemek::ui
|
||||||
|
|
||||||
if (underline && current_underline)
|
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)
|
else if (underline && !current_underline)
|
||||||
{
|
{
|
||||||
current_underline = x_range;
|
current_underline = {x_range, chunk.color};
|
||||||
}
|
}
|
||||||
else if (!underline && current_underline)
|
else if (!underline && current_underline)
|
||||||
{
|
{
|
||||||
|
|
@ -482,11 +521,17 @@ namespace psemek::ui
|
||||||
|
|
||||||
if (strikethrough && current_strikethrough)
|
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)
|
else if (strikethrough && !current_strikethrough)
|
||||||
{
|
{
|
||||||
current_strikethrough = x_range;
|
current_strikethrough = {x_range, chunk.color};
|
||||||
}
|
}
|
||||||
else if (!strikethrough && current_strikethrough)
|
else if (!strikethrough && current_strikethrough)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue