#include #include #include namespace psemek::ui { label::label(std::string text) : text_(std::move(text)) {} void label::set_text(std::string text) { text_ = std::move(text); 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(); } void label::set_overflow(overflow_mode value) { overflow_ = value; on_state_changed(); } void label::reshape(geom::box const & bbox) { shape_.box = bbox; cached_state_.reset(); } geom::box label::size_constraints() const { static float const inf = std::numeric_limits::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 label::width_constraints(float height) const { static float const inf = std::numeric_limits::infinity(); auto state = cached_state_for({{{0.f, inf}, {0.f, height}}}); return {state.size[0], inf}; } geom::interval label::height_constraints(float width) const { static float const inf = std::numeric_limits::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(*st->text_shadow_offset); for (auto const & batch : cached_state_->batches) 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}); } 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 const & bbox) const { auto state = cached_state{}; if (text_.empty()) return state; auto st = merged_own_style(); if (!st) return state; auto font = st->text_style->is_set(text_style_flag::bold) ? st->bold_font.get() : st->font.get(); bool const underline = st->text_style->is_set(text_style_flag::underline); bool const strikethrough = st->text_style->is_set(text_style_flag::strikethrough); if (!font) return state; shape_options opts; opts.scale = *st->text_scale; auto glyphs = font->shape(text_, opts); geom::box raw_bbox; for (auto const & g : glyphs) raw_bbox |= g.position; std::size_t max_lines = 1; if (wrap_) { max_lines = std::isfinite(bbox[1].length()) ? std::max(1, std::floor(bbox[1].length() / font->size()[1] / (*st->text_scale))) : std::numeric_limits::max(); } std::vector> lines; std::size_t current_glyph = 0; for (std::size_t line = 0; line < max_lines; ++line) { if (line != 0) ++current_glyph; if (skip_spaces_) { while (current_glyph < glyphs.size() && std::isspace(glyphs[current_glyph].character)) ++current_glyph; } std::size_t line_begin = current_glyph; std::size_t line_end = line_begin; geom::interval x_range; while (line_end < glyphs.size()) { if (is_newline(glyphs[line_end].character)) break; x_range |= glyphs[line_end].position[0]; if (x_range.length() > bbox[0].length()) break; ++line_end; } if (line_end < glyphs.size()) { std::size_t space_pos = line_end; while (space_pos > line_begin && !std::isspace(glyphs[space_pos].character)) --space_pos; if (space_pos > line_begin) line_end = space_pos; } if (line + 1 == max_lines && line_end < glyphs.size()) { switch (overflow_) { case overflow_mode::ignore: line_end = glyphs.size(); break; case overflow_mode::drop: glyphs.erase(glyphs.begin() + line_end, glyphs.end()); break; case overflow_mode::ellipsis: { std::size_t el_start = std::max(3, line_end) - 3; float x_offset = 0.f; if (el_start > 0) x_offset = glyphs[el_start - 1].position[0].max; char const el_str[] = "..."; auto els = font->shape(el_str, opts); for (std::size_t i = el_start; i < line_end; ++i) { glyphs[i] = els[i - el_start]; glyphs[i].position[0] += x_offset; } glyphs.erase(glyphs.begin() + line_end, glyphs.end()); } break; } } current_glyph = line_end; lines.push_back({line_begin, line_end}); if (line_end == glyphs.size()) break; } float max_line_size = 0.f; float const underline_width = *(st->text_scale); float const strikethrough_width = *(st->text_scale); std::vector> underline_box(lines.size()); std::vector> strikethrough_box(lines.size()); for (std::size_t l = 0; l < lines.size(); ++l) { geom::interval x_range; int spaces = 0; for (std::size_t i = lines[l].first; i < lines[l].second; ++i) { x_range |= glyphs[i].position[0]; if (std::isspace(glyphs[i].character)) ++spaces; } max_line_size = std::max(max_line_size, x_range.length()); geom::vector offset; float space_extra = 0.f; switch (halign_) { case halignment::left: offset[0] = bbox[0].min - x_range.min; break; case halignment::center: offset[0] = bbox[0].center() - x_range.length() / 2.f - x_range.min; break; case halignment::right: offset[0] = bbox[0].max - x_range.length() - x_range.min; break; case halignment::stretch: offset[0] = bbox[0].min - x_range.min; if ((l + 1 != lines.size() && text_[lines[l].second] != '\n') && spaces > 0) space_extra = (bbox[0].length() - x_range.length()) / spaces; break; } switch (valign_) { case valignment::top: offset[1] = bbox[1].min + l * (*st->text_scale) * font->size()[1]; break; case valignment::center: offset[1] = bbox[1].center() + (l - lines.size() / 2.f) * (*st->text_scale) * font->size()[1]; break; case valignment::bottom: offset[1] = bbox[1].max + (l - lines.size() * 1.f) * (*st->text_scale) * font->size()[1]; break; } underline_box[l][0] = x_range + offset[0]; underline_box[l][1].min = offset[1] + font->size()[1] * (*st->text_scale); underline_box[l][1].max = underline_box[l][1].min + underline_width; strikethrough_box[l][0] = x_range + offset[0]; strikethrough_box[l][1].min = offset[1] + font->size()[1] * (*st->text_scale) / 2.f; strikethrough_box[l][1].max = strikethrough_box[l][1].min + strikethrough_width; for (std::size_t i = lines[l].first; i < lines[l].second; ++i) { glyphs[i].position += offset; if (std::isspace(glyphs[i].character)) offset[0] += space_extra; } } { auto & batch = state.batches.emplace_back(); batch.texture = &font->atlas(); for (auto const & g : glyphs) { auto tc = font->texcoords(g.character); if (!tc) continue; auto & image = batch.images.emplace_back(); image.position = g.position; image.position[0] += (std::round(image.position[0].min) - image.position[0].min); image.position[1] += (std::round(image.position[1].min) - image.position[1].min); image.texcoords = *tc; } } if (underline) { auto & batch = state.batches.emplace_back(); batch.texture = single_white_pixel_texture().get(); for (auto const & u : underline_box) { auto & image = batch.images.emplace_back(); image.position = u; image.texcoords = {{{0.f, 1.f}, {0.f, 1.f}}}; } } if (strikethrough) { auto & batch = state.batches.emplace_back(); batch.texture = single_white_pixel_texture().get(); for (auto const & u : strikethrough_box) { auto & image = batch.images.emplace_back(); image.position = u; image.texcoords = {{{0.f, 1.f}, {0.f, 1.f}}}; } } state.size[0] = max_line_size; state.size[1] = lines.size() * (*st->text_scale) * font->size()[1]; return state; } std::shared_ptr label::single_white_pixel_texture() const { static std::weak_ptr texture; if (!single_white_pixel_texture_) { std::shared_ptr ptr; if (!(ptr = texture.lock())) { ptr = std::make_shared(); 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_; } }