#include "../include/Body.hpp" #include "../include/QuickMedia.hpp" #include "../include/Scale.hpp" #include "../plugins/Plugin.hpp" #include #include #include static const sf::Color front_color(32, 36, 42); static const sf::Color back_color(33, 35, 37); static float image_max_height = 100.0f; static const float spacing_y = 15.0f; static const float padding_x = 10.0f; static const float image_padding_x = 5.0f; static const float padding_y = 5.0f; static const float embedded_item_padding_y = 0.0f; namespace QuickMedia { BodyItem::BodyItem(std::string _title) : visible(true), dirty(false), dirty_description(false), dirty_author(false), dirty_timestamp(false), thumbnail_is_local(false), title_color(sf::Color::White), author_color(sf::Color::White), userdata(nullptr), last_drawn_time(0), timestamp(0) { if(!_title.empty()) set_title(std::move(_title)); } Body::Body(Program *program, sf::Font *font, sf::Font *bold_font, sf::Font *cjk_font) : font(font), bold_font(bold_font), cjk_font(cjk_font), progress_text("", *font, 14), replies_text("", *font, 14), embedded_item_load_text("", *font, 14), draw_thumbnails(false), wrap_around(false), line_seperator_color(sf::Color(32, 37, 43, 255)), program(program), selected_item(0), prev_selected_item(0), page_scroll(0.0f), item_background(sf::Vector2f(1.0f, 1.0f), 10.0f, 10), num_visible_items(0), last_item_fully_visible(true), last_fully_visible_item(-1) { progress_text.setFillColor(sf::Color::White); replies_text.setFillColor(sf::Color(129, 162, 190)); thumbnail_resize_target_size.x = 200; thumbnail_resize_target_size.y = 119; thumbnail_fallback_size.x = 50.0f; thumbnail_fallback_size.y = 100.0f; image_fallback.setFillColor(sf::Color::White); item_background.setFillColor(sf::Color(55, 60, 68)); } // TODO: Make this work with wraparound enabled? // TODO: For plugins with different sized body items this can be weird, because after scrolling down thumbnails could load and they could move items up/down until we see items we haven't seen bool Body::select_previous_page() { for(int i = 0; i < num_visible_items - 1; ++i) { if(!select_previous_item()) return false; } return true; } // TODO: Make this work with wraparound enabled? // TODO: For plugins with different sized body items this can be weird, because after scrolling down thumbnails could load and they could move items up/down until we see items we haven't seen bool Body::select_next_page() { for(int i = 0; i < num_visible_items - 1; ++i) { if(!select_next_item()) return false; } return true; } bool Body::select_previous_item() { if(items.empty()) return false; int new_selected_item = selected_item; int num_items = (int)items.size(); for(int i = 0; i < num_items; ++i) { if(new_selected_item - 1 < 0) { if(wrap_around) new_selected_item = num_items - 1; else { new_selected_item = selected_item; break; } } else { --new_selected_item; } if(items[new_selected_item]->visible) break; } if(selected_item == new_selected_item) return false; selected_item = new_selected_item; return true; } bool Body::select_next_item() { if(items.empty()) return false; int new_selected_item = selected_item; int num_items = (int)items.size(); for(int i = 0; i < num_items; ++i) { if(new_selected_item + 1 == num_items) { if(wrap_around) { new_selected_item = 0; } else { new_selected_item = selected_item; break; } } else { ++new_selected_item; } if(items[new_selected_item]->visible) break; } if(selected_item == new_selected_item) return false; selected_item = new_selected_item; return true; } void Body::set_selected_item(int item) { assert(item >= 0 && item < (int)items.size()); selected_item = item; prev_selected_item = selected_item; clamp_selection(); //page_scroll = 0.0f; } int Body::get_index_by_body_item(BodyItem *body_item) { for(int i = 0; i < (int)items.size(); ++i) { if(items[i].get() == body_item) return i; } return -1; } void Body::select_first_item() { selected_item = 0; prev_selected_item = selected_item; page_scroll = 0.0f; clamp_selection(); } void Body::select_last_item() { selected_item = std::max(0, (int)items.size() - 1); //prev_selected_item = selected_item; //page_scroll = 0.0f; clamp_selection(); } void Body::reset_selected() { select_first_item(); } void Body::clear_items() { items.clear(); selected_item = 0; prev_selected_item = selected_item; page_scroll = 0.0f; } // TODO: Optimize with memcpy and changing capacity before loop void Body::prepend_items(BodyItems new_items) { for(auto &body_item : new_items) { items.insert(items.begin(), std::move(body_item)); } } // TODO: Optimize with memcpy and changing capacity before loop void Body::append_items(BodyItems new_items) { for(auto &body_item : new_items) { items.push_back(std::move(body_item)); } } void Body::insert_item_by_timestamp(std::shared_ptr body_item) { for(size_t i = 0; i < items.size(); ++i) { if(body_item->get_timestamp() < items[i]->get_timestamp()) { items.insert(items.begin() + i, std::move(body_item)); return; } } items.push_back(std::move(body_item)); } // TODO: Optimize by resizing |items| before insert void Body::insert_items_by_timestamps(BodyItems new_items) { for(auto &new_item : new_items) { insert_item_by_timestamp(std::move(new_item)); } } void Body::clear_cache() { clear_text_cache(); clear_thumbnails(); } void Body::clear_text_cache() { for(auto &body_item : items) { clear_body_item_cache(body_item.get()); } } void Body::clear_thumbnails() { item_thumbnail_textures.clear(); } BodyItem* Body::get_selected() const { if(selected_item < 0 || selected_item >= (int)items.size() || !items[selected_item]->visible) return nullptr; return items[selected_item].get(); } std::shared_ptr Body::get_selected_shared() { if(selected_item < 0 || selected_item >= (int)items.size() || !items[selected_item]->visible) return nullptr; return items[selected_item]; } BodyItem* Body::get_last_fully_visible_item() { if(last_fully_visible_item < 0 || last_fully_visible_item >= (int)items.size() || !items[last_fully_visible_item]->visible) return nullptr; return items[last_fully_visible_item].get(); } void Body::clamp_selection() { int num_items = (int)items.size(); if(items.empty()) return; if(selected_item < 0) selected_item = 0; else if(selected_item >= num_items) selected_item = num_items - 1; for(int i = selected_item; i >= 0; --i) { if(items[i]->visible) { selected_item = i; return; } } for(int i = selected_item + 1; i < num_items; ++i) { if(items[i]->visible) { selected_item = i; return; } } } void Body::draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size) { draw(window, pos, size, Json::Value::nullSingleton()); } // TODO: Use a render target for the whole body so all images can be put into one. // TODO: Load thumbnails with more than one thread. void Body::draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size, const Json::Value &content_progress) { sf::Vector2f scissor_pos = pos; sf::Vector2f scissor_size = size; const float start_y = pos.y; const sf::Int32 elapsed_time = draw_timer.getElapsedTime().asMilliseconds(); if(thumbnail_resize_target_size.x != 0 && thumbnail_resize_target_size.y != 0) { image_max_height = thumbnail_resize_target_size.y; } //item_background.setFillColor(front_color); //item_background.setOutlineThickness(1.0f); //item_background.setOutlineColor(sf::Color(13, 15, 17)); image_fallback.setSize(thumbnail_fallback_size); item_background_shadow.setFillColor(line_seperator_color); num_visible_items = 0; last_item_fully_visible = true; last_fully_visible_item = -1; int num_items = items.size(); if(num_items == 0 || size.y <= 0.0f) { item_thumbnail_textures.clear(); for(auto &body_item : items) { clear_body_item_cache(body_item.get()); } return; } for(auto &thumbnail_it : item_thumbnail_textures) { thumbnail_it.second->referenced = false; } if(prev_selected_item < 0 || prev_selected_item >= (int)items.size()) { prev_selected_item = selected_item; } // TODO: Optimize this, especially when scrolling to top/bottom. // TODO: Test when wrapping is enabled int selected_item_diff = selected_item - prev_selected_item; int selected_int_diff_abs = std::abs(selected_item_diff); if(selected_item_diff > 0) { int num_items_scrolled = 0; int i = prev_selected_item; while(num_items_scrolled < selected_int_diff_abs && i < num_items) { if(items[i]->visible) { page_scroll += (get_item_height(items[i].get(), selected_int_diff_abs < 50) + spacing_y); } ++num_items_scrolled; ++i; } prev_selected_item = selected_item; } else if(selected_item_diff < 0) { int num_items_scrolled = 0; int i = prev_selected_item - 1; while(num_items_scrolled < selected_int_diff_abs && i >= 0) { if(items[i]->visible) { page_scroll -= (get_item_height(items[i].get(), selected_int_diff_abs < 50) + spacing_y); } ++num_items_scrolled; --i; } prev_selected_item = selected_item; } float selected_item_height = get_item_height(items[selected_item].get()) + spacing_y; if(page_scroll > size.y - selected_item_height) { page_scroll = size.y - selected_item_height; } else if(page_scroll < 0.0f) { page_scroll = 0.0f; } pos.y += page_scroll; sf::Vector2u window_size = window.getSize(); sf::Vector2f prev_pos = pos; for(int i = selected_item - 1; i >= 0; --i) { auto &item = items[i]; // TODO: Find a better solution? if(!item->visible) continue; update_dirty_state(item.get(), size); item->last_drawn_time = elapsed_time; float item_height = get_item_height(item.get()); prev_pos.y -= (item_height + spacing_y); if(prev_pos.y + item_height + spacing_y < start_y) break; // This is needed here rather than above the loop, since update_dirty_text cant be called inside scissor because it corrupts the text for some reason glEnable(GL_SCISSOR_TEST); glScissor(scissor_pos.x, (int)window_size.y - (int)scissor_pos.y - (int)scissor_size.y, scissor_size.x, scissor_size.y); draw_item(window, item.get(), prev_pos, size, item_height, i, content_progress); glDisable(GL_SCISSOR_TEST); ++num_visible_items; } sf::Vector2f after_pos = pos; for(int i = selected_item; i < num_items; ++i) { auto &item = items[i]; // TODO: Find a better solution? if(!item->visible) continue; update_dirty_state(item.get(), size); item->last_drawn_time = elapsed_time; float item_height = get_item_height(item.get()); if((after_pos.y - start_y) + item_height + spacing_y > size.y) last_item_fully_visible = false; else last_fully_visible_item = i; if(after_pos.y - start_y >= size.y) break; // This is needed here rather than above the loop, since update_dirty_text cant be called inside scissor because it corrupts the text for some reason glEnable(GL_SCISSOR_TEST); glScissor(scissor_pos.x, (int)window_size.y - (int)scissor_pos.y - (int)scissor_size.y, scissor_size.x, scissor_size.y); draw_item(window, item.get(), after_pos, size, item_height, i, content_progress); glDisable(GL_SCISSOR_TEST); after_pos.y += item_height + spacing_y; ++num_visible_items; } if(last_fully_visible_item == -1) last_fully_visible_item = selected_item; for(auto it = item_thumbnail_textures.begin(); it != item_thumbnail_textures.end();) { if(!it->second->referenced) it = item_thumbnail_textures.erase(it); else ++it; } // TODO: Only do this for items that are not visible, do not loop all items. // TODO: Verify if this only runs for items that are not visible, and only once // TODO: Improve performance! right now it can use up to 5-7% cpu with a lot of items! for(auto &body_item : items) { if(elapsed_time - body_item->last_drawn_time >= 1500) { clear_body_item_cache(body_item.get()); } } } void Body::update_dirty_state(BodyItem *body_item, sf::Vector2f size) { if(body_item->dirty) { body_item->dirty = false; // TODO: Find a way to optimize fromUtf8 sf::String str = sf::String::fromUtf8(body_item->get_title().data(), body_item->get_title().data() + body_item->get_title().size()); if(body_item->title_text) body_item->title_text->setString(std::move(str)); else body_item->title_text = std::make_unique(std::move(str), font, cjk_font, 16, size.x - 50 - image_padding_x * 2.0f); body_item->title_text->setFillColor(body_item->title_color); body_item->title_text->updateGeometry(); } if(body_item->dirty_description) { body_item->dirty_description = false; sf::String str = sf::String::fromUtf8(body_item->get_description().data(), body_item->get_description().data() + body_item->get_description().size()); if(body_item->description_text) body_item->description_text->setString(std::move(str)); else body_item->description_text = std::make_unique(std::move(str), font, cjk_font, 14, size.x - 50 - image_padding_x * 2.0f); body_item->description_text->updateGeometry(); } if(body_item->dirty_author) { body_item->dirty_author = false; sf::String str = sf::String::fromUtf8(body_item->get_author().data(), body_item->get_author().data() + body_item->get_author().size()); if(body_item->author_text) body_item->author_text->setString(std::move(str)); else body_item->author_text = std::make_unique(std::move(str), bold_font, cjk_font, 14, size.x - 50 - image_padding_x * 2.0f); body_item->author_text->setFillColor(body_item->author_color); body_item->author_text->updateGeometry(); } if(body_item->dirty_timestamp) { body_item->dirty_timestamp = false; //time_t time_now = time(NULL); //struct tm *now_tm = localtime(&time_now); time_t message_timestamp = body_item->get_timestamp() / 1000; struct tm *message_tm = localtime(&message_timestamp); //bool is_same_year = message_tm->tm_year == now_tm->tm_year; char time_str[128] = {0}; /* if(is_same_year) strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M:%S", message_tm); else strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M:%S %Y", message_tm); */ strftime(time_str, sizeof(time_str) - 1, "%H:%M:%S", message_tm); if(body_item->timestamp_text) body_item->timestamp_text->setString(time_str); else body_item->timestamp_text = std::make_unique(time_str, *font, 14); body_item->timestamp_text->setFillColor(sf::Color(185, 190, 198)); } } void Body::clear_body_item_cache(BodyItem *body_item) { if(body_item->title_text) { body_item->title_text.reset(); body_item->dirty = true; } if(body_item->description_text) { body_item->description_text.reset(); body_item->dirty_description = true; } if(body_item->author_text) { body_item->author_text.reset(); body_item->dirty_author = true; } if(body_item->timestamp_text) { body_item->timestamp_text.reset(); body_item->dirty_timestamp = true; } } void Body::draw_item(sf::RenderWindow &window, BodyItem *item, sf::Vector2f pos, sf::Vector2f size, bool include_embedded_item) { update_dirty_state(item, size); item->last_drawn_time = draw_timer.getElapsedTime().asMilliseconds(); sf::Vector2u window_size = window.getSize(); glEnable(GL_SCISSOR_TEST); glScissor(pos.x, (int)window_size.y - (int)pos.y - (int)size.y, size.x, size.y); draw_item(window, item, pos, size, size.y + spacing_y, -1, Json::Value::nullSingleton(), include_embedded_item); glDisable(GL_SCISSOR_TEST); } // TODO: Better message? maybe fallback to the reply message, or message status (such as message redacted) static const char* embedded_item_status_to_string(EmbeddedItemStatus embedded_item_status) { switch(embedded_item_status) { case EmbeddedItemStatus::NONE: return ""; case EmbeddedItemStatus::LOADING: return "Loading message..."; case EmbeddedItemStatus::FINISHED_LOADING: return "Finished loading message..."; case EmbeddedItemStatus::FAILED_TO_LOAD: return "Failed to load message!"; } return ""; } void Body::draw_item(sf::RenderWindow &window, BodyItem *item, const sf::Vector2f &pos, const sf::Vector2f &size, const float item_height, const int item_index, const Json::Value &content_progress, bool include_embedded_item) { // TODO: Instead of generating a new hash everytime to access textures, cache the hash of the thumbnail url std::shared_ptr item_thumbnail; if(draw_thumbnails) { auto item_thumbnail_it = item_thumbnail_textures.find(item->thumbnail_url); if(item_thumbnail_it == item_thumbnail_textures.end()) { item_thumbnail = std::make_shared(); item_thumbnail_textures.insert(std::make_pair(item->thumbnail_url, item_thumbnail)); } else { item_thumbnail = item_thumbnail_it->second; } item_thumbnail->referenced = true; } if(body_item_render_callback) body_item_render_callback(item); sf::Vector2f item_pos; item_pos.x = std::floor(pos.x); item_pos.y = std::floor(pos.y); item_background_shadow.setSize(sf::Vector2f(std::max(0.0f, size.x - 20.0f), 1.0f)); item_background_shadow.setPosition(item_pos + sf::Vector2f(10.0f, std::floor(item_height + spacing_y * 0.5f))); window.draw(item_background_shadow); if(item_index == selected_item) { item_background.setPosition(item_pos); item_background.setSize(sf::Vector2f(size.x, item_height)); window.draw(item_background); } float text_offset_x = padding_x; if(draw_thumbnails) { // TODO: Verify if this is safe. The thumbnail is being modified in another thread // and it might not be fully finished before the native handle is set? if(item_thumbnail->loading_state == LoadingState::APPLIED_TO_TEXTURE && item_thumbnail->texture.getNativeHandle() != 0) { image.setTexture(item_thumbnail->texture, true); auto image_size = image.getTexture()->getSize(); auto height_ratio = std::min(image_max_height, (float)image_size.y) / image_size.y; auto scale = image.getScale(); auto image_scale_ratio = scale.x / scale.y; const float width_ratio = height_ratio * image_scale_ratio; image.setScale(width_ratio, height_ratio); image.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y)); window.draw(image); text_offset_x += image_padding_x + width_ratio * image_size.x; // We want the next image fallback to have the same size as the successful image rendering, because its likely the image fallback will have the same size (for example thumbnails on youtube) //image_fallback.setSize(sf::Vector2f(width_ratio * image_size.x, height_ratio * image_size.y)); } else if(!item->thumbnail_url.empty()) { image_fallback.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y)); window.draw(image_fallback); text_offset_x += image_padding_x + image_fallback.getSize().x; } } const float timestamp_text_y = std::floor(item_pos.y + padding_y - 6.0f); if(item->author_text) { item->author_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 6.0f)); item->author_text->setMaxWidth(size.x - text_offset_x - image_padding_x); item->author_text->draw(window); sf::Vector2f replies_text_pos = item->author_text->getPosition() + sf::Vector2f(0.0f, 5.0f); replies_text_pos.x += item->author_text->getWidth() + 5.0f; replies_text.setPosition(replies_text_pos); sf::String replies_text_str; for(size_t reply_index : item->replies) { BodyItem *reply_item = items[reply_index].get(); replies_text_str += ">>"; replies_text_str += reply_item->post_number; } replies_text.setString(std::move(replies_text_str)); window.draw(replies_text); item_pos.y += item->author_text->getHeight() - 2.0f; } if(include_embedded_item && item->embedded_item_status != EmbeddedItemStatus::NONE) { float embedded_item_height = item->embedded_item ? get_item_height(item->embedded_item.get(), true, false) : (embedded_item_load_text.getLocalBounds().height + embedded_item_padding_y * 2.0f); const float border_width = 4.0f; sf::RectangleShape border_left(sf::Vector2f(border_width, std::floor(embedded_item_height))); border_left.setFillColor(sf::Color::White); border_left.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + 4.0f)); window.draw(border_left); if(item->embedded_item) { sf::Vector2f embedded_item_pos(std::floor(item_pos.x + text_offset_x + border_width + padding_x), std::floor(item_pos.y + embedded_item_padding_y + 4.0f)); sf::Vector2f embedded_item_size(std::floor(size.x - text_offset_x - border_width - padding_x), embedded_item_height); draw_item(window, item->embedded_item.get(), embedded_item_pos, embedded_item_size, false); } else { embedded_item_load_text.setString(embedded_item_status_to_string(item->embedded_item_status)); embedded_item_load_text.setPosition(std::floor(item_pos.x + text_offset_x + border_width + padding_x), std::floor(item_pos.y + embedded_item_height * 0.5f - embedded_item_load_text.getLocalBounds().height * 0.5f + 4.0f)); window.draw(embedded_item_load_text); } item_pos.y += embedded_item_height + 4.0f; } //title_text.setString(item->title); //title_text.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y)); //window.draw(title_text); if(item->title_text) { item->title_text->setFillColor(item->title_color); item->title_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 6.0f)); item->title_text->setMaxWidth(size.x - text_offset_x - image_padding_x); item->title_text->draw(window); item_pos.y += item->title_text->getHeight() - 2.0f; } if(item->description_text) { float height_offset = 0.0f; item->description_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 6.0f + height_offset)); item->description_text->setMaxWidth(size.x - text_offset_x - image_padding_x); item->description_text->draw(window); item_pos.y += item->description_text->getHeight() - 2.0f; } if(item->timestamp_text) { item->timestamp_text->setPosition(std::floor(item_pos.x + size.x - item->timestamp_text->getLocalBounds().width - padding_x), timestamp_text_y + 4.0f); window.draw(*item->timestamp_text); } if(!content_progress.isObject()) return; // TODO: Do the same for non-manga content. // TODO: Cache this instead of hash access every item every frame. const Json::Value &item_progress = content_progress[item->get_title()]; if(item_progress.isObject()) { const Json::Value ¤t_json = item_progress["current"]; const Json::Value &total_json = item_progress["total"]; if(current_json.isNumeric() && total_json.isNumeric()) { progress_text.setString(std::string("Page: ") + std::to_string(current_json.asInt()) + "/" + std::to_string(total_json.asInt())); auto bounds = progress_text.getLocalBounds(); progress_text.setPosition(std::floor(item_pos.x + size.x - bounds.width - padding_x), timestamp_text_y + 6.0f); window.draw(progress_text); } } } float Body::get_item_height(BodyItem *item, bool load_texture, bool include_embedded_item) { float item_height = 0.0f; if(item->title_text) { item_height += item->title_text->getHeight() - 2.0f; } if(item->author_text) { item_height += item->author_text->getHeight() - 2.0f; } if(include_embedded_item && item->embedded_item_status != EmbeddedItemStatus::NONE) { if(item->embedded_item) item_height += (get_item_height(item->embedded_item.get(), load_texture, false) + 4.0f + embedded_item_padding_y * 2.0f); else item_height += (embedded_item_load_text.getLocalBounds().height + 4.0f + embedded_item_padding_y * 2.0f); } if(item->description_text) { item_height += item->description_text->getHeight() - 2.0f; } if(draw_thumbnails && !item->thumbnail_url.empty()) { float image_height = image_fallback.getSize().y; std::shared_ptr item_thumbnail; auto item_thumbnail_it = item_thumbnail_textures.find(item->thumbnail_url); if(item_thumbnail_it == item_thumbnail_textures.end()) { if(load_texture) { item_thumbnail = std::make_shared(); item_thumbnail_textures.insert(std::make_pair(item->thumbnail_url, item_thumbnail)); } } else { item_thumbnail = item_thumbnail_it->second; } if(load_texture) { item_thumbnail->referenced = true; if(!item->thumbnail_url.empty() && item_thumbnail->loading_state == LoadingState::NOT_LOADED) { async_image_loader.load_thumbnail(item->thumbnail_url, item->thumbnail_is_local, thumbnail_resize_target_size, program->is_tor_enabled(), item_thumbnail); } if(item_thumbnail->loading_state == LoadingState::FINISHED_LOADING && item_thumbnail->image->getSize().x > 0 && item_thumbnail->image->getSize().y > 0) { if(!item_thumbnail->texture.loadFromImage(*item_thumbnail->image)) fprintf(stderr, "Warning: failed to load texture from image: %s\n", item->thumbnail_url.c_str()); //item_thumbnail->texture.setSmooth(true); //item_thumbnail->texture.generateMipmap(); item_thumbnail->image.reset(); item_thumbnail->loading_state = LoadingState::APPLIED_TO_TEXTURE; } } if(item_thumbnail && item_thumbnail->loading_state == LoadingState::APPLIED_TO_TEXTURE && item_thumbnail->texture.getNativeHandle() != 0) { auto image_size = item_thumbnail->texture.getSize(); image_height = std::min(image_max_height, (float)image_size.y); } item_height = std::max(item_height, image_height); } return item_height + padding_y * 2.0f; } float Body::get_spacing_y() const { return spacing_y; } //static bool Body::string_find_case_insensitive(const std::string &str, const std::string &substr) { auto it = std::search(str.begin(), str.end(), substr.begin(), substr.end(), [](char c1, char c2) { return std::toupper(c1) == std::toupper(c2); }); return it != str.end(); } void Body::filter_search_fuzzy(const std::string &text) { if(text.empty()) { for(auto &item : items) { item->visible = true; } return; } for(auto &item : items) { item->visible = string_find_case_insensitive(item->get_title(), text); if(!item->visible && !item->get_description().empty()) item->visible = string_find_case_insensitive(item->get_description(), text); } select_first_item(); } bool Body::no_items_visible() const { for(auto &item : items) { if(item->visible) return false; } return true; } bool Body::is_selected_item_last_visible_item() const { if(selected_item < 0 || selected_item >= (int)items.size() || !items[selected_item]->visible) return false; for(int i = selected_item + 1; i < (int)items.size(); ++i) { if(items[i]->visible) return false; } return true; } }