#include #include #include #include namespace psemek::ui { bool edit::set_text(std::string_view text, bool signal) { return set_text(util::from_utf8(std::string(text)), signal); } bool edit::set_text(std::u32string text, bool signal) { if (validator_ && !validator_(text)) return false; text_ = std::move(text); caret_ = std::min(caret_, text_.size()); on_state_changed(); post_text_changed(); if (signal) post_text_entered(); return true; } void edit::set_font(font_type f) { if (font_ != f) { font_ = f; on_state_changed(); } } void edit::set_halign(halignment value) { if (halign_ != value) { halign_ = value; on_state_changed(); } } void edit::set_valign(valignment value) { if (valign_ != value) { valign_ = value; on_state_changed(); } } bool edit::set_validator(validator_type validator) { if (validator && !validator(text_)) return false; validator_ = std::move(validator); return true; } edit::validator_type edit::numeric(int max_length) { return [max_length](std::u32string_view const str) -> bool { auto begin = str.begin(); auto end = str.end(); if (begin != end && *begin == '-') ++begin; return std::all_of(begin, end, [](char32_t c){ return '0' <= c && c <= '9'; }) && (end - begin) <= max_length; }; } edit::validator_type edit::numeric_nonnegative(int max_length) { return [max_length](std::u32string_view const str) -> bool { return std::all_of(str.begin(), str.end(), [](char32_t c){ return '0' <= c && c <= '9'; }) && str.length() <= max_length; }; } void edit::on_text_entered(callback_type callback, bool signal) { on_text_entered_ = std::move(callback); if (signal) post_text_entered(); } void edit::on_start_input(std::function callback) { on_start_input_ = std::move(callback); } void edit::on_text_changed(callback_type callback) { on_text_changed_ = std::move(callback); } bool edit::on_event(mouse_move const & e) { static auto beam_cursor = sdl2::get_default_cursor(sdl2::default_cursor_type::beam); static auto arrow_cursor = sdl2::get_default_cursor(sdl2::default_cursor_type::arrow); auto m = math::cast(e.position); mouse_x_ = m[0]; bool new_mouseover = shape_.contains(m); if (!mouseover_ && new_mouseover) sdl2::set_cursor(*beam_cursor); else if (mouseover_ && !new_mouseover) sdl2::set_cursor(*arrow_cursor); mouseover_ = new_mouseover; return false; } bool edit::on_event(mouse_click const & e) { if (e.button == mouse_button::left && e.down) { if (mouseover_) { if (!editing_) { editing_ = true; old_text_ = text_; start_text_input(); reset_caret(); post_start_input(); } if (cached_state_ && mouse_x_) { float x_offset = 0.f; switch (halign_) { case halignment::left: x_offset = text_box_[0].min; break; case halignment::center: x_offset = text_box_[0].center() - cached_state_->size[0] * 0.5f; break; case halignment::right: x_offset = text_box_[0].max - cached_state_->size[0]; break; } auto it = std::lower_bound(cached_state_->images.begin(), cached_state_->images.end(), *mouse_x_, [&](auto const & i, float x){ return i.position[0].center() + x_offset < x; }); caret_ = it - cached_state_->images.begin(); caret_blink_timer_ = 0.f; caret_visible_ = true; } return true; } else { if (editing_) { editing_ = false; stop_text_input(); reset_caret(); post_text_entered(); } } } if (e.button == mouse_button::right && e.down) { if (editing_) { editing_ = false; text_ = old_text_; cached_state_.reset(); stop_text_input(); reset_caret(); caret_ = std::min(caret_, text_.size()); post_text_entered(); return true; } } return false; } bool edit::on_event(key_press const & e) { if (e.key == SDLK_LCTRL) ctrl_down_ = e.down; if (e.down && editing_) { if (e.key == SDLK_LEFT) { if (ctrl_down_) { std::size_t start = caret_; while (start > 0 && std::isspace(text_[start - 1])) --start; while (start > 0 && !std::isspace(text_[start - 1])) --start; caret_ = start; reset_caret(); } else { if (caret_ > 0) { --caret_; reset_caret(); } } } else if (e.key == SDLK_RIGHT) { if (ctrl_down_) { std::size_t end = caret_; while (end < text_.size() && !std::isspace(text_[end])) ++end; while (end < text_.size() && std::isspace(text_[end])) ++end; caret_ = end; reset_caret(); } else { if (caret_ < text_.size()) { ++caret_; reset_caret(); } } } else if (e.key == SDLK_HOME) { caret_ = 0; reset_caret(); } else if (e.key == SDLK_END) { caret_ = text_.size(); reset_caret(); } else if (e.key == SDLK_RETURN) { if (editing_) { editing_ = false; stop_text_input(); reset_caret(); post_text_entered(); } } else if (e.key == SDLK_ESCAPE) { if (editing_) { editing_ = false; text_ = old_text_; cached_state_.reset(); stop_text_input(); reset_caret(); caret_ = std::min(caret_, text_.size()); post_text_changed(); post_text_entered(); } } else if (e.key == SDLK_BACKSPACE) { if (ctrl_down_) { std::size_t start = caret_; while (start > 0 && std::isspace(text_[start - 1])) --start; while (start > 0 && !std::isspace(text_[start - 1])) --start; auto new_text = text_; new_text.erase(start, caret_ - start); if (set_text(std::move(new_text), false)) { caret_ = start; reset_caret(); } } else { if (caret_ > 0) { --caret_; auto new_text = text_; new_text.erase(new_text.begin() + caret_); if (!set_text(std::move(new_text), false)) ++caret_; reset_caret(); } } } else if (e.key == SDLK_DELETE) { if (ctrl_down_) { std::size_t end = caret_; while (end < text_.size() && !std::isspace(text_[end])) ++end; while (end < text_.size() && std::isspace(text_[end])) ++end; auto new_text = text_; new_text.erase(caret_, end - caret_); if (set_text(std::move(new_text), false)) { reset_caret(); } } else { if (caret_ < text_.size()) { auto new_text = text_; new_text.erase(new_text.begin() + caret_); set_text(std::move(new_text), false); reset_caret(); } } } return true; } return false; } bool edit::on_event(text_input const & e) { if (editing_) { auto new_text = text_; auto range = util::utf8_range(e.text); new_text.insert(new_text.begin() + caret_, range.begin(), range.end()); if (set_text(std::move(new_text), false)) caret_ += range.size(); reset_caret(); return true; } return false; } bool edit::focused() const { return editing_; } bool edit::editing() const { return editing_; } void edit::reshape(math::box const & bbox) { shape_.box = bbox; text_box_ = bbox; } math::box edit::size_constraints() const { // TODO: min x-size static float const inf = std::numeric_limits::infinity(); auto st = merged_own_style(); return {{{0.f, inf}, {1.f * st->font->size()[1] * (*st->scale), inf}}}; } void edit::update(float dt) { if (editing_) { caret_blink_timer_ += dt; if (caret_blink_timer_ >= caret_blink_period_) { caret_blink_timer_ -= caret_blink_period_; caret_visible_ = !caret_visible_; } } } void edit::draw(painter & p) const { auto st = merged_own_style(); if (!cached_state_) { auto font = (font_ == font_type::bold) ? st->bold_font.get() : st->font.get(); cached_state state; state.texture = &font->atlas(); fonts::shape_options options; options.scale = *st->text_scale; auto glyphs = font->shape(text_, options); math::box bbox; for (auto const & g : glyphs) bbox |= g.position; state.size[0] = glyphs.empty() ? 0.f : bbox[0].length(); state.size[1] = font->size()[1] * (*st->text_scale); for (auto const & g : glyphs) { auto tc = font->texcoords(g.character); if (!tc) continue; state.images.push_back({*tc, g.position}); } cached_state_ = std::move(state); } math::vector offset{0.f, 0.f}; switch (halign_) { case halignment::left: offset[0] = text_box_[0].min; break; case halignment::center: offset[0] = text_box_[0].center() - cached_state_->size[0] * 0.5f; break; case halignment::right: offset[0] = text_box_[0].max - cached_state_->size[0]; break; } switch (valign_) { case valignment::top: offset[1] = text_box_[1].min; break; case valignment::center: offset[1] = text_box_[1].center() - cached_state_->size[1] * 0.5f; break; case valignment::bottom: offset[1] = text_box_[1].max - cached_state_->size[1]; break; } offset[0] = std::round(offset[0]); offset[1] = std::round(offset[1]); p.begin_stencil(); p.draw_rect(text_box_, {0, 0, 0, 255}); p.commit_stencil(); if (*st->text_shadow_offset != math::vector{0, 0} && (*st->shadow_color)[3] != 0) { auto shoffset = offset + math::cast(*st->text_shadow_offset); for (auto const & i : cached_state_->images) p.draw_image(i.position + shoffset, {cached_state_->texture, i.texcoords}, {*st->shadow_color, painter::color_mode::multiply}); } for (auto const & i : cached_state_->images) p.draw_image(i.position + offset, {cached_state_->texture, i.texcoords}, {*st->text_color, painter::color_mode::multiply}); p.end_stencil(); if (caret_visible_) { float x; if (cached_state_->images.empty()) x = 0.f; else if (caret_ == 0) x = cached_state_->images.front().position[0].min; else if (caret_ >= cached_state_->images.size()) x = cached_state_->images.back().position[0].max; else x = (cached_state_->images[caret_ - 1].position[0].max + cached_state_->images[caret_].position[0].min) / 2.f; x += offset[0]; float y = offset[1]; p.draw_rect({{{x - (*st->text_scale) * 0.5f, x + (*st->text_scale) * 0.5f}, {y, y + cached_state_->size[1]}}}, *st->text_color); } } void edit::set_text_shape(math::box const & box) { text_box_ = box; } void edit::on_state_changed() { cached_state_ = std::nullopt; post_reshape(); reset_caret(); } void edit::reset_caret() { caret_visible_ = editing_; caret_blink_timer_ = 0.f; } void edit::post_text_entered() const { if (on_text_entered_) { post([weak_self = weak_from_this(), cb = on_text_entered_]{ if (auto self = weak_self.lock()) cb(static_cast(self.get())->text_); }); } } void edit::post_start_input() const { if (on_start_input_) post(on_start_input_); } void edit::post_text_changed() const { if (on_text_changed_) { post([weak_self = weak_from_this(), cb = on_text_changed_]{ if (auto self = weak_self.lock()) cb(static_cast(self.get())->text_); }); } } }