#include #include #include 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{}}); 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 (chunks_.empty()) return state; auto st = merged_own_style(); auto const font_height = st->font->size()[1] * (*st->text_scale); // Shape glyphs chunk by chunk // All glyphs within a chunk have // the exact same font properties struct glyph_chunk { std::size_t end; text_style style; }; std::vector glyphs; std::vector glyph_chunks; { geom::point pen{0.f, 0.f}; shape_options opts; opts.scale = *st->text_scale; for (auto const & chunk : chunks_) { if (auto text = std::get_if(&chunk)) { if (text->text.find("yo") != std::string::npos) { int x = 42; (void)x; } auto const merged_text_style = *st->text_style | text->style; bool const bold = merged_text_style.is_set(text_style_flag::bold); auto font = bold ? st->bold_font.get() : st->font.get(); auto chunk_glyphs = font->shape(text->text, opts, pen); 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}); } } } // Break glyphs into lines struct line_range { std::size_t begin; std::size_t end; }; std::vector lines; { std::size_t current_glyph = 0; while (current_glyph < glyphs.size()) { if (skip_spaces_) { while (current_glyph < glyphs.size() && std::isspace(glyphs[current_glyph].character)) ++current_glyph; } std::size_t line_begin = current_glyph; bool newline_end = false; bool wrap_end = false; geom::interval x_range; while (current_glyph < glyphs.size()) { if (is_newline(glyphs[current_glyph].character)) { newline_end = true; break; } x_range |= glyphs[current_glyph].position[0]; if (x_range.length() > bbox[0].length() && wrap_) { wrap_end = true; break; } ++current_glyph; } if (wrap_end && current_glyph > line_begin) { std::size_t space_pos = current_glyph - 1; while (space_pos > line_begin && !std::isspace(glyphs[space_pos].character)) --space_pos; if (space_pos > line_begin) current_glyph = space_pos; } lines.push_back({line_begin, current_glyph}); if (newline_end) ++current_glyph; } } // Compute line bboxes std::vector> line_bbox(lines.size()); for (std::size_t l = 0; l < lines.size(); ++l) { geom::box 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 |= glyphs[i].position; line_bbox[l] = bbox; } // Compute total text size geom::vector 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 & glyphs inside label bbox std::vector> 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 (std::isspace(glyphs[i].character)) ++spaces; geom::vector 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) { glyphs[i].position += offset; if (std::isspace(glyphs[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 : glyph_chunks) { auto & batch = state.batches.emplace_back(); auto font = ch.style.is_set(text_style_flag::bold) ? st->bold_font.get() : st->font.get(); batch.texture = &font->atlas(); for (std::size_t i = begin; i < ch.end; ++i) { auto & g = glyphs[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 & 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) { 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; auto flush_underline = [&]{ auto & image = batch.images.emplace_back(); image.position[0] = *current_underline; 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; image.position[1] = {strikethrough_y, strikethrough_y + line_width}; current_strikethrough = std::nullopt; }; for (; ch < glyph_chunks.size(); ++ch) { auto const & chunk = glyph_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 x_range; for (std::size_t i = ibegin; i < iend; ++i) x_range |= glyphs[i].position[0]; if (underline && current_underline) { *current_underline |= x_range; } else if (underline && !current_underline) { current_underline = x_range; } else if (!underline && current_underline) { flush_underline(); } if (strikethrough && current_strikethrough) { *current_strikethrough |= x_range; } else if (strikethrough && !current_strikethrough) { current_strikethrough = x_range; } else if (!strikethrough && current_strikethrough) { flush_strikethrough(); } } if (chunk.end > lines[l].end) break; ch_begin = chunk.end; } if (current_underline) flush_underline(); if (current_strikethrough) flush_strikethrough(); } } state.size = text_size; 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_; } }