758 lines
18 KiB
C++
758 lines
18 KiB
C++
#include <psemek/ui/label.hpp>
|
|
|
|
#include <psemek/geom/contains.hpp>
|
|
|
|
#include <psemek/sdl2/cursor.hpp>
|
|
|
|
#include <stdexcept>
|
|
#include <cctype>
|
|
#include <unordered_map>
|
|
|
|
namespace psemek::ui
|
|
{
|
|
|
|
label::label(std::string text)
|
|
{
|
|
set_text(std::move(text));
|
|
}
|
|
|
|
void label::set_text(std::string text)
|
|
{
|
|
text_ = std::move(text);
|
|
|
|
chunks_.clear();
|
|
chunks_.push_back(text_chunk{{text_.data(), text_.size()}, text_style{}, std::nullopt, std::nullopt});
|
|
|
|
on_state_changed();
|
|
}
|
|
|
|
static std::unordered_set<std::string_view> const known_tags
|
|
{
|
|
"bold",
|
|
"uline",
|
|
"strike",
|
|
"color",
|
|
"link",
|
|
};
|
|
|
|
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) + "\"");
|
|
}
|
|
else if (tag == "link")
|
|
{
|
|
if (!attribute)
|
|
throw std::runtime_error("tag [link] requires an attribute");
|
|
}
|
|
}
|
|
|
|
void label::set_tagged_text(std::string text)
|
|
{
|
|
text_ = std::move(text);
|
|
selected_link_ = std::nullopt;
|
|
|
|
chunks_.clear();
|
|
|
|
auto parse_result = tagged_text::parse(text_);
|
|
|
|
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"].empty())
|
|
chunk.style.set(text_style_flag::bold);
|
|
if (!tags_stack["uline"].empty())
|
|
chunk.style.set(text_style_flag::underline);
|
|
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());
|
|
if (!tags_stack["link"].empty())
|
|
chunk.link = tags_stack["link"].back();
|
|
chunk.text = *text;
|
|
chunks_.push_back(chunk);
|
|
}
|
|
else if (auto tag = std::get_if<tagged_text::opening_tag>(&token))
|
|
{
|
|
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());
|
|
if (!tags_stack["link"].empty())
|
|
chunk.link = tags_stack["link"].back();
|
|
chunks_.push_back(chunk);
|
|
}
|
|
else if (known_tags.contains(tag->type))
|
|
{
|
|
check_attribute(tag->type, tag->attribute);
|
|
if (tag->type == "link" && !tags_stack["link"].empty())
|
|
throw std::runtime_error("tag [link] cannot be nested");
|
|
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 (!known_tags.contains(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();
|
|
}
|
|
}
|
|
|
|
on_state_changed();
|
|
}
|
|
|
|
void label::set_halign(halignment value)
|
|
{
|
|
halign_ = value;
|
|
on_state_changed();
|
|
}
|
|
|
|
void label::set_valign(valignment value)
|
|
{
|
|
valign_ = value;
|
|
on_state_changed();
|
|
}
|
|
|
|
void label::set_wrap(bool value)
|
|
{
|
|
wrap_ = value;
|
|
on_state_changed();
|
|
}
|
|
|
|
void label::set_skip_spaces(bool value)
|
|
{
|
|
skip_spaces_ = value;
|
|
on_state_changed();
|
|
}
|
|
|
|
image_provider * label::set_image_provider(struct image_provider * provider)
|
|
{
|
|
std::swap(provider, image_provider_);
|
|
on_state_changed();
|
|
return provider;
|
|
}
|
|
|
|
void label::on_link_click(link_callback callback)
|
|
{
|
|
link_callback_ = std::move(callback);
|
|
}
|
|
|
|
void label::set_overflow(overflow_mode value)
|
|
{
|
|
overflow_ = value;
|
|
on_state_changed();
|
|
}
|
|
|
|
void label::reshape(geom::box<float, 2> const & bbox)
|
|
{
|
|
shape_.box = bbox;
|
|
cached_state_.reset();
|
|
}
|
|
|
|
bool label::on_event(ui::mouse_move const & e)
|
|
{
|
|
if (cached_state_)
|
|
{
|
|
std::optional<std::string_view> new_selected_link;
|
|
auto const p = geom::cast<float>(e.position);
|
|
for (auto const & b : cached_state_->link_bboxes)
|
|
{
|
|
if (geom::contains(b.first, p))
|
|
{
|
|
new_selected_link = b.second;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (new_selected_link != selected_link_)
|
|
{
|
|
if (new_selected_link)
|
|
sdl2::set_cursor(sdl2::cursor_type::hand);
|
|
else
|
|
sdl2::set_cursor(sdl2::cursor_type::arrow);
|
|
|
|
selected_link_ = new_selected_link;
|
|
mouse_down_ = false;
|
|
on_state_changed();
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool label::on_event(ui::mouse_click const & e)
|
|
{
|
|
if (e.button == mouse_button::left)
|
|
{
|
|
if (e.down)
|
|
{
|
|
if (selected_link_)
|
|
{
|
|
if (!mouse_down_)
|
|
{
|
|
mouse_down_ = true;
|
|
if (link_callback_)
|
|
post([cb = link_callback_, text = *selected_link_]{ cb(text); });
|
|
on_state_changed();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (mouse_down_)
|
|
{
|
|
mouse_down_ = false;
|
|
on_state_changed();
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
geom::box<float, 2> label::size_constraints() const
|
|
{
|
|
static float const inf = std::numeric_limits<float>::infinity();
|
|
|
|
if (!cached_state_inf_)
|
|
cached_state_inf_ = cached_state_for({{{0.f, inf}, {0.f, inf}}});
|
|
|
|
return {{{cached_state_inf_->size[0], inf}, {cached_state_inf_->size[1], inf}}};
|
|
}
|
|
|
|
geom::interval<float> label::width_constraints(float height) const
|
|
{
|
|
static float const inf = std::numeric_limits<float>::infinity();
|
|
|
|
auto state = cached_state_for({{{0.f, inf}, {0.f, height}}});
|
|
return {state.size[0], inf};
|
|
}
|
|
|
|
geom::interval<float> label::height_constraints(float width) const
|
|
{
|
|
static float const inf = std::numeric_limits<float>::infinity();
|
|
|
|
auto state = cached_state_for({{{0.f, width}, {0.f, inf}}});
|
|
return {state.size[1], inf};
|
|
}
|
|
|
|
void label::style_updated() const
|
|
{
|
|
element::style_updated();
|
|
cached_state_.reset();
|
|
cached_state_inf_.reset();
|
|
}
|
|
|
|
void label::own_style_updated() const
|
|
{
|
|
element::own_style_updated();
|
|
cached_state_.reset();
|
|
cached_state_inf_.reset();
|
|
}
|
|
|
|
void label::draw(painter & p) const
|
|
{
|
|
if (!cached_state_)
|
|
update_cached_state();
|
|
|
|
auto st = merged_own_style();
|
|
if (!st) return;
|
|
|
|
if (st->text_shadow_offset != geom::vector{0, 0})
|
|
{
|
|
auto const offset = geom::cast<float>(*st->text_shadow_offset);
|
|
for (auto const & batch : cached_state_->batches)
|
|
{
|
|
if (!batch.text) continue;
|
|
auto color = *st->shadow_color;
|
|
color[3] = (color[3] * 1.f * batch.color[3]) / 255.f;
|
|
for (auto const & image : batch.images)
|
|
p.draw_image(image.position + offset, gfx::texture_view_2d{batch.texture, image.texcoords}, {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}, {batch.color, batch.text ? painter::color_mode::multiply : painter::color_mode::mix});
|
|
}
|
|
|
|
void label::on_state_changed()
|
|
{
|
|
cached_state_.reset();
|
|
cached_state_inf_.reset();
|
|
post_reshape();
|
|
}
|
|
|
|
static bool is_newline(char32_t c)
|
|
{
|
|
return (c == '\n') || (c == '\r');
|
|
}
|
|
|
|
void label::update_cached_state() const
|
|
{
|
|
cached_state_ = cached_state_for(shape_.box);
|
|
}
|
|
|
|
label::cached_state label::cached_state_for(geom::box<float, 2> const & bbox) const
|
|
{
|
|
auto state = cached_state{};
|
|
|
|
if (chunks_.empty()) return state;
|
|
|
|
auto st = merged_own_style();
|
|
|
|
auto const font_height = st->font->size()[1] * (*st->text_scale);
|
|
|
|
// Shape items (glyphs/images) chunk by chunk
|
|
// All items within a chunk have
|
|
// the exact same properties
|
|
|
|
struct item
|
|
{
|
|
geom::box<float, 2> position;
|
|
std::optional<char32_t> character;
|
|
};
|
|
|
|
struct item_chunk
|
|
{
|
|
std::size_t end;
|
|
text_style style;
|
|
gfx::color_rgba color;
|
|
gfx::texture_view_2d image;
|
|
std::optional<std::string_view> link;
|
|
};
|
|
|
|
std::vector<item> items;
|
|
std::vector<item_chunk> item_chunks;
|
|
|
|
{
|
|
geom::point<float, 2> pen{0.f, 0.f};
|
|
shape_options opts;
|
|
opts.scale = *st->text_scale;
|
|
|
|
for (auto const & chunk : chunks_)
|
|
{
|
|
if (auto text = std::get_if<text_chunk>(&chunk))
|
|
{
|
|
auto text_style = *st->text_style | text->style;
|
|
|
|
if (text->link)
|
|
{
|
|
if (selected_link_ && *text->link == *selected_link_)
|
|
{
|
|
if (mouse_down_)
|
|
text_style |= *st->link_click_style;
|
|
else
|
|
text_style |= *st->link_hover_style;
|
|
}
|
|
else
|
|
text_style |= *st->link_style;
|
|
}
|
|
|
|
gfx::color_rgba color;
|
|
if (text->color)
|
|
color = *text->color;
|
|
else if (text->link)
|
|
{
|
|
if (selected_link_ && *text->link == *selected_link_)
|
|
{
|
|
if (mouse_down_)
|
|
color = *st->link_click_color;
|
|
else
|
|
color = *st->link_hover_color;
|
|
}
|
|
else
|
|
color = *st->link_color;
|
|
}
|
|
else
|
|
color = *st->text_color;
|
|
|
|
bool const bold = text_style.is_set(text_style_flag::bold);
|
|
|
|
auto font = bold ? st->bold_font.get() : st->font.get();
|
|
|
|
auto glyphs = font->shape(text->text, opts, pen);
|
|
|
|
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(), text_style, color, {}, text->link});
|
|
}
|
|
else if (auto image = std::get_if<image_chunk>(&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<float, 2> 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, image->link});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Break items into lines
|
|
|
|
struct line_range
|
|
{
|
|
std::size_t begin;
|
|
std::size_t end;
|
|
};
|
|
|
|
std::vector<line_range> lines;
|
|
|
|
{
|
|
std::size_t current_item = 0;
|
|
while (true)
|
|
{
|
|
if (skip_spaces_)
|
|
{
|
|
while (current_item < items.size() && items[current_item].character && std::isspace(*items[current_item].character))
|
|
++current_item;
|
|
}
|
|
|
|
std::size_t line_begin = current_item;
|
|
|
|
bool newline_end = false;
|
|
bool wrap_end = false;
|
|
|
|
geom::interval<float> x_range;
|
|
while (current_item < items.size())
|
|
{
|
|
if (items[current_item].character && is_newline(*items[current_item].character))
|
|
{
|
|
newline_end = true;
|
|
break;
|
|
}
|
|
|
|
x_range |= items[current_item].position[0];
|
|
|
|
if (x_range.length() > bbox[0].length() && wrap_)
|
|
{
|
|
wrap_end = true;
|
|
break;
|
|
}
|
|
|
|
++current_item;
|
|
}
|
|
|
|
if (wrap_end && current_item == line_begin)
|
|
break;
|
|
|
|
if (wrap_end && current_item > line_begin)
|
|
{
|
|
std::size_t space_pos = current_item - 1;
|
|
while (space_pos > line_begin && (!items[space_pos].character || !std::isspace(*items[space_pos].character)))
|
|
--space_pos;
|
|
|
|
if (space_pos > line_begin)
|
|
current_item = space_pos;
|
|
}
|
|
|
|
lines.push_back({line_begin, current_item});
|
|
|
|
if (current_item == items.size())
|
|
break;
|
|
|
|
if (newline_end)
|
|
++current_item;
|
|
}
|
|
}
|
|
|
|
// Compute line bboxes
|
|
|
|
std::vector<geom::box<float, 2>> line_bbox(lines.size());
|
|
for (std::size_t l = 0; l < lines.size(); ++l)
|
|
{
|
|
geom::box<float, 2> bbox;
|
|
// Glyphs might be smaller than total text height, so
|
|
// 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 |= items[i].position;
|
|
line_bbox[l] = bbox;
|
|
}
|
|
|
|
// Compute total text size
|
|
|
|
geom::vector<float, 2> text_size{0.f, 0.f};
|
|
for (auto const & l : line_bbox)
|
|
{
|
|
text_size[0] = std::max(text_size[0], l[0].length());
|
|
text_size[1] += l[1].length();
|
|
}
|
|
|
|
// TODO: handle text overflow
|
|
|
|
// Position lines & items inside label bbox
|
|
|
|
std::vector<geom::vector<float, 2>> line_offset(lines.size());
|
|
{
|
|
float current_y;
|
|
switch (valign_)
|
|
{
|
|
case valignment::top:
|
|
current_y = bbox[1].min;
|
|
break;
|
|
case valignment::center:
|
|
current_y = bbox[1].center() - text_size[1] / 2.f;
|
|
break;
|
|
case valignment::bottom:
|
|
current_y = bbox[1].max - text_size[1];
|
|
break;
|
|
}
|
|
|
|
for (std::size_t l = 0; l < lines.size(); ++l)
|
|
{
|
|
int spaces = 0;
|
|
|
|
for (std::size_t i = lines[l].begin; i < lines[l].end; ++i)
|
|
if (items[i].character && std::isspace(*items[i].character))
|
|
++spaces;
|
|
|
|
geom::vector<float, 2> offset{0.f, current_y};
|
|
float space_extra = 0.f;
|
|
|
|
switch (halign_)
|
|
{
|
|
case halignment::left:
|
|
offset[0] = bbox[0].min - line_bbox[l][0].min;
|
|
break;
|
|
case halignment::center:
|
|
offset[0] = bbox[0].center() - line_bbox[l][0].length() / 2.f - line_bbox[l][0].min;
|
|
break;
|
|
case halignment::right:
|
|
offset[0] = bbox[0].max - line_bbox[l][0].length() - line_bbox[l][0].min;
|
|
break;
|
|
case halignment::stretch:
|
|
offset[0] = bbox[0].min - line_bbox[l][0].min;
|
|
if ((l + 1 != lines.size() && text_[lines[l].end] != '\n') && spaces > 0)
|
|
space_extra = (bbox[0].length() - line_bbox[l][0].length()) / spaces;
|
|
break;
|
|
}
|
|
|
|
line_offset[l] = offset;
|
|
|
|
for (std::size_t i = lines[l].begin; i < lines[l].end; ++i)
|
|
{
|
|
items[i].position += offset;
|
|
if (items[i].character && std::isspace(*items[i].character))
|
|
offset[0] += space_extra;
|
|
}
|
|
|
|
current_y += line_bbox[l][1].length();
|
|
}
|
|
}
|
|
|
|
// Convert chunks into image batches
|
|
|
|
{
|
|
std::size_t begin = 0;
|
|
for (auto const & ch : item_chunks)
|
|
{
|
|
auto & batch = state.batches.emplace_back();
|
|
|
|
if (ch.image)
|
|
{
|
|
batch.texture = ch.image.texture;
|
|
batch.color = ch.color;
|
|
batch.text = false;
|
|
|
|
for (std::size_t i = begin; i < ch.end; ++i)
|
|
{
|
|
auto & g = items[i];
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Convert chunks into underline & strikethrough batches
|
|
|
|
{
|
|
float const line_width = 1.f * (*st->text_scale);
|
|
|
|
auto get_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;
|
|
b.text = true;
|
|
}
|
|
return state.batches.back();
|
|
};
|
|
|
|
std::size_t ch_begin = 0;
|
|
std::size_t ch = 0;
|
|
for (std::size_t l = 0; l < lines.size(); ++l)
|
|
{
|
|
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<float> last_underline_end;
|
|
std::optional<float> last_strikethrough_end;
|
|
|
|
for (; ch < item_chunks.size(); ++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);
|
|
|
|
std::size_t ibegin = std::max(ch_begin, lines[l].begin);
|
|
std::size_t iend = std::min(chunk.end, lines[l].end);
|
|
|
|
if (ibegin < iend)
|
|
{
|
|
geom::interval<float> x_range;
|
|
for (std::size_t i = ibegin; i < iend; ++i)
|
|
x_range |= items[i].position[0];
|
|
|
|
if (underline)
|
|
{
|
|
auto & image = get_batch(chunk.color).images.emplace_back();
|
|
image.position[0] = x_range;
|
|
if (last_underline_end)
|
|
image.position[0] |= *last_underline_end;
|
|
image.position[1] = {underline_y, underline_y + line_width};
|
|
last_underline_end = image.position[0].max;
|
|
}
|
|
else
|
|
last_underline_end = std::nullopt;
|
|
|
|
if (strikethrough)
|
|
{
|
|
auto & image = get_batch(chunk.color).images.emplace_back();
|
|
image.position[0] = x_range;
|
|
if (last_strikethrough_end)
|
|
image.position[0] |= *last_strikethrough_end;
|
|
image.position[1] = {strikethrough_y, strikethrough_y + line_width};
|
|
last_strikethrough_end = image.position[0].max;
|
|
}
|
|
else
|
|
last_strikethrough_end = std::nullopt;
|
|
}
|
|
|
|
if (chunk.end > lines[l].end)
|
|
break;
|
|
|
|
ch_begin = chunk.end;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate link bboxes
|
|
{
|
|
std::size_t begin = 0;
|
|
for (auto const & ch : item_chunks)
|
|
{
|
|
if (ch.link)
|
|
{
|
|
geom::box<float, 2> bbox;
|
|
for (std::size_t i = begin; i < ch.end; ++i)
|
|
bbox |= items[i].position;
|
|
|
|
state.link_bboxes.push_back({bbox, *ch.link});
|
|
}
|
|
|
|
begin = ch.end;
|
|
}
|
|
}
|
|
|
|
state.size = text_size;
|
|
|
|
return state;
|
|
}
|
|
|
|
std::shared_ptr<gfx::texture_2d> label::single_white_pixel_texture() const
|
|
{
|
|
static std::weak_ptr<gfx::texture_2d> texture;
|
|
|
|
if (!single_white_pixel_texture_)
|
|
{
|
|
std::shared_ptr<gfx::texture_2d> ptr;
|
|
|
|
if (!(ptr = texture.lock()))
|
|
{
|
|
ptr = std::make_shared<gfx::texture_2d>();
|
|
|
|
gfx::pixmap_rgba pm({1, 1}, {255, 255, 255, 255});
|
|
ptr->load(pm);
|
|
ptr->nearest_filter();
|
|
texture = ptr;
|
|
}
|
|
|
|
single_white_pixel_texture_ = ptr;
|
|
}
|
|
|
|
return single_white_pixel_texture_;
|
|
}
|
|
|
|
}
|