#include "../include/Body.hpp" #include "../include/QuickMedia.hpp" #include "../include/Scale.hpp" #include "../include/ResourceLoader.hpp" #include "../include/AsyncImageLoader.hpp" #include "../include/Utils.hpp" #include "../include/StringUtils.hpp" #include "../plugins/Plugin.hpp" #include #include #include #include #include #include static const sf::Color front_color(32, 36, 42); static const sf::Color back_color(33, 35, 37); static const float spacing_y = std::floor(10.0f * QuickMedia::get_ui_scale()); static const float padding_x = std::floor(10.0f * QuickMedia::get_ui_scale()); static const float image_padding_x = std::floor(5.0f * QuickMedia::get_ui_scale()); static const float padding_y = std::floor(5.0f * QuickMedia::get_ui_scale()); static const float embedded_item_padding_y = std::floor(0.0f * QuickMedia::get_ui_scale()); static const double thumbnail_fade_duration_sec = 0.1; static const float reaction_background_padding_x = std::floor(7.0f * QuickMedia::get_ui_scale()); static const float reaction_background_padding_y = std::floor(3.0f * QuickMedia::get_ui_scale()); static const float reaction_spacing_x = std::floor(5.0f * QuickMedia::get_ui_scale()); static const float reaction_padding_y = std::floor(7.0f * QuickMedia::get_ui_scale()); static const int embedded_item_font_size = std::floor(14 * QuickMedia::get_ui_scale()); namespace QuickMedia { BodyItem::BodyItem(std::string _title) : visible(true), dirty(false), dirty_description(false), dirty_author(false), dirty_timestamp(false), thumbnail_is_local(false), userdata(nullptr), last_drawn_time(0.0), timestamp(0), title_color(sf::Color::White), author_color(sf::Color::White), description_color(sf::Color::White) { if(!_title.empty()) set_title(std::move(_title)); } BodyItem& BodyItem::operator=(BodyItem &other) { url = other.url; thumbnail_url = other.thumbnail_url; attached_content_url = other.attached_content_url; visible = other.visible; dirty = other.dirty; dirty_description = other.dirty_description; dirty_author = other.dirty_author; dirty_timestamp = other.dirty_timestamp; thumbnail_is_local = other.thumbnail_is_local; if(other.title_text) title_text = std::make_unique(*other.title_text); else title_text = nullptr; if(other.description_text) description_text = std::make_unique(*other.description_text); else description_text = nullptr; if(other.author_text) author_text = std::make_unique(*other.author_text); else author_text = nullptr; if(other.timestamp_text) timestamp_text = std::make_unique(*other.timestamp_text); else timestamp_text = nullptr; replies = other.replies; post_number = other.post_number; userdata = other.userdata; last_drawn_time = other.last_drawn_time; embedded_item_status = other.embedded_item_status; if(other.embedded_item) { embedded_item.reset(new BodyItem("")); *embedded_item = *other.embedded_item; } else { embedded_item = nullptr; } thumbnail_mask_type = other.thumbnail_mask_type; thumbnail_size = other.thumbnail_size; reactions.clear(); for(auto &reaction : other.reactions) { Reaction reaction_copy; reaction_copy.text = std::make_unique(*reaction.text); reaction_copy.userdata = reaction.userdata; reactions.push_back(std::move(reaction_copy)); } title = other.title; description = other.description; author = other.author; timestamp = other.timestamp; title_color = other.title_color; author_color = other.author_color; description_color = other.description_color; extra = other.extra; calculated_height = other.calculated_height; return *this; } void BodyItem::add_reaction(std::string text, void *userdata) { sf::String str = sf::String::fromUtf8(text.begin(), text.end()); Reaction reaction; reaction.text = std::make_unique(std::move(str), false, std::floor(14 * get_ui_scale()), 0.0f); reaction.userdata = userdata; reactions.push_back(std::move(reaction)); } Body::Body(Program *program, sf::Texture &loading_icon_texture, sf::Shader *rounded_rectangle_shader) : progress_text("", *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(14 * get_ui_scale())), replies_text("", *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(14 * get_ui_scale())), embedded_item_load_text("", *FontLoader::get_font(FontLoader::FontType::LATIN), embedded_item_font_size), draw_thumbnails(true), wrap_around(false), line_separator_color(sf::Color(32, 37, 43, 255)), body_item_render_callback(nullptr), thumbnail_mask_shader(nullptr), program(program), selected_item(0), prev_selected_item(0), page_scroll(0.0f), loading_icon(loading_icon_texture), num_visible_items(0), first_item_fully_visible(true), last_item_fully_visible(true), first_fully_visible_item(-1), last_fully_visible_item(-1), item_background(sf::Vector2f(1.0f, 1.0f), 10.0f, sf::Color(55, 60, 68), rounded_rectangle_shader), reaction_background(sf::Vector2f(1.0f, 1.0f), 10.0f, sf::Color(33, 37, 44), rounded_rectangle_shader) { assert(rounded_rectangle_shader); progress_text.setFillColor(sf::Color::White); replies_text.setFillColor(sf::Color(129, 162, 190)); thumbnail_max_size.x = 250; thumbnail_max_size.y = 141; sf::Vector2f loading_icon_size(loading_icon.getTexture()->getSize().x, loading_icon.getTexture()->getSize().y); loading_icon.setOrigin(loading_icon_size.x * 0.5f, loading_icon_size.y * 0.5f); } Body::~Body() { item_thumbnail_textures.clear(); items.clear(); malloc_trim(0); } // 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() { if(!selected_line_top_visible) return select_previous_item(false); for(int i = 0; i < num_visible_items - 1; ++i) { if(!select_previous_item(false)) 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() { if(!selected_line_bottom_visible) return select_next_item(false); for(int i = 0; i < num_visible_items - 1; ++i) { if(!select_next_item(false)) return false; } return true; } bool Body::select_previous_item(bool scroll_page_if_large_item, bool reset_select_scroll) { if(items.empty()) return false; clamp_selected_item_to_body_count = 1; if(scroll_page_if_large_item && !selected_line_top_visible) { page_scroll += 128.0f; return true; } 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(reset_select_scroll) selected_scrolled = 0.0f; if(selected_item == new_selected_item) return false; selected_item = new_selected_item; return true; } bool Body::select_next_item(bool scroll_page_if_large_item, bool reset_select_scroll) { if(items.empty()) return false; clamp_selected_item_to_body_count = 1; if(scroll_page_if_large_item && !selected_line_bottom_visible) { page_scroll -= 128.0f; return true; } 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(reset_select_scroll) selected_scrolled = 0.0f; if(selected_item == new_selected_item) return false; selected_item = new_selected_item; return true; } void Body::set_selected_item(int item, bool reset_prev_selected_item) { //assert(item >= 0 && item < (int)items.size()); if(item != selected_item) selected_scrolled = 0.0f; selected_item = item; clamp_selection(); if(reset_prev_selected_item) prev_selected_item = selected_item; clamp_selected_item_to_body_count = 1; //page_scroll = 0.0f; } void Body::reset_prev_selected_item() { prev_selected_item = selected_item; } 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_scrolled = 0.0f; selected_item = 0; prev_selected_item = selected_item; page_scroll = 0.0f; clamp_selection(); clamp_selected_item_to_body_count = 1; } void Body::select_last_item() { int new_selected_item = std::max(0, (int)items.size() - 1); selected_scrolled = 0.0f; selected_item = new_selected_item; //prev_selected_item = selected_item; //page_scroll = 0.0f; clamp_selection(); clamp_selected_item_to_body_count = 1; } void Body::clear_items() { if(selected_item != 0) selected_scrolled = 0.0f; items.clear(); selected_item = 0; prev_selected_item = selected_item; page_scroll = 0.0f; clamp_selected_item_to_body_count = 1; } void Body::prepend_items(BodyItems new_items) { items.insert(items.begin(), std::make_move_iterator(new_items.begin()), std::make_move_iterator(new_items.end())); } void Body::append_items(BodyItems new_items) { items.insert(items.end(), std::make_move_iterator(new_items.begin()), std::make_move_iterator(new_items.end())); } // TODO: Binary search and use hint to start search from start or end (for example when adding "previous" items or "next" items) 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(new_item); } } void Body::clear_cache() { clear_text_cache(); clear_thumbnails(); malloc_trim(0); } void Body::clear_text_cache() { for(auto &body_item : items) { clear_body_item_cache(body_item.get()); if(body_item->embedded_item) clear_body_item_cache(body_item->embedded_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(); } BodyItem* Body::get_last_visible_item() { if(last_visible_item < 0 || last_visible_item >= (int)items.size() || !items[last_visible_item]->visible) return nullptr; return items[last_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; } } } bool Body::on_event(const sf::RenderWindow &window, const sf::Event &event) { if(event.type == sf::Event::Resized) clamp_selected_item_to_body_count = 1; if(!is_touch_enabled()) return false; if(event.type == sf::Event::MouseButtonPressed && event.mouseButton.button == sf::Mouse::Left && !mouse_left_pressed && sf::FloatRect(body_pos, body_size).contains(sf::Vector2f(event.mouseButton.x, event.mouseButton.y))) { mouse_left_pressed = true; mouse_pos_raw.x = event.mouseButton.x; mouse_pos_raw.y = event.mouseButton.y; mouse_pos = sf::Vector2f(mouse_pos_raw.x, mouse_pos_raw.y); prev_mouse_pos = mouse_pos; mouse_click_pos = mouse_pos; mouse_press_pixels_moved_abs = 0.0; has_scrolled_with_input = true; return true; } else if(event.type == sf::Event::MouseButtonReleased && event.mouseButton.button == sf::Mouse::Left && mouse_left_pressed) { mouse_left_pressed = false; mouse_left_clicked = true; mouse_release_pos = sf::Vector2f(event.mouseButton.x, event.mouseButton.y); return true; } else if(event.type == sf::Event::MouseMoved && mouse_left_pressed) { sf::Vector2i mouse_pos_diff(event.mouseMove.x - mouse_pos_raw.x, event.mouseMove.y - mouse_pos_raw.y); mouse_press_pixels_moved_abs += std::sqrt(mouse_pos_diff.x*mouse_pos_diff.x + mouse_pos_diff.y*mouse_pos_diff.y); mouse_pos_raw.x = event.mouseMove.x; mouse_pos_raw.y = event.mouseMove.y; return true; } return false; } 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; body_pos = pos; body_size = size; elapsed_time_sec = draw_timer.getElapsedTime().asSeconds(); bool keep_selected_inside_body = clamp_selected_item_to_body_count > 0 || offset_to_top > 0.1f || offset_to_bottom > 0.1f; if(has_scrolled_with_input) { clamp_selected_item_to_body_count--; if(clamp_selected_item_to_body_count < 0) clamp_selected_item_to_body_count = 0; } if(is_touch_enabled()) { float frame_time = frame_timer.restart().asSeconds(); if(frame_time > 2.0f) frame_time = 2.0f; const sf::Vector2f mouse_pos_diff(mouse_pos_raw.x - mouse_pos.x, mouse_pos_raw.y - mouse_pos.y); const float move_speed = 25.0f; mouse_pos.x += (mouse_pos_diff.x * frame_time * move_speed); mouse_pos.y += (mouse_pos_diff.y * frame_time * move_speed); sf::Vector2f mouse_smooth_diff(mouse_pos.x - prev_mouse_pos.x, mouse_pos.y - prev_mouse_pos.y); prev_mouse_pos = mouse_pos; if(items_cut_off) { if(mouse_left_pressed) { selected_scrolled += mouse_smooth_diff.y; page_scroll += mouse_smooth_diff.y; mouse_scroll_accel = mouse_smooth_diff; } else { selected_scrolled += mouse_scroll_accel.y; page_scroll += mouse_scroll_accel.y; } } if(mouse_scroll_accel.y > 0.1 && first_fully_visible_item == -1) { keep_selected_inside_body = true; } else if(mouse_scroll_accel.y < -0.1 && last_fully_visible_item == -1) { keep_selected_inside_body = true; } if(mouse_scroll_accel.y > 0.1 && first_fully_visible_item != -1) { selected_item = first_fully_visible_item; clamp_selection(); } else if(mouse_scroll_accel.y < -0.1 && last_fully_visible_item != -1) { selected_item = last_fully_visible_item; clamp_selection(); } if(!mouse_left_pressed) { const float scroll_deaccel = 1.02f; double deaccel = scroll_deaccel * (1.0 + frame_time); if(deaccel < 0.0001) deaccel = 1.0; mouse_scroll_accel.x /= deaccel; if(fabs(mouse_scroll_accel.x) < 0.0001) mouse_scroll_accel.x = 0.0; mouse_scroll_accel.y /= deaccel; if(fabs(mouse_scroll_accel.y) < 0.0001) mouse_scroll_accel.y = 0.0; } } item_separator.setFillColor(line_separator_color); num_visible_items = 0; first_fully_visible_item = -1; last_fully_visible_item = -1; selected_line_top_visible = true; selected_line_bottom_visible = true; 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()); if(body_item->embedded_item) clear_body_item_cache(body_item->embedded_item.get()); } first_item_fully_visible = true; last_item_fully_visible = true; items_cut_off = false; offset_to_top = 0.0f; offset_to_bottom = 0.0f; mouse_left_clicked = false; last_visible_item = -1; 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: If the thumbnail fallback size is larger than the real thumbnail size then the scroll will be incorrect when scrolling down // because first we scroll by a larger value and then the thumbnail size changes when the thumbnail has finished loading // and the selected item will no longer be exactly at the bottom of the window, and we will see parts of then next item. // To fix this, we should detect when the selected item is at the bottom of the screen and the thumbnail changes from image fallback to the real thumbnail // and add the difference in size to scroll, if the real thumbnail size is smaller than the image fallback. 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; BodyItem *prev_body_item = get_previous_visible_item(i); while(num_items_scrolled < selected_int_diff_abs && i < num_items) { if(items[i]->visible) { const bool merge_with_previous = body_item_merge_handler && body_item_merge_handler(prev_body_item, items[i].get()); page_scroll += get_item_height(items[i].get(), size.x, selected_int_diff_abs < 50, true, merge_with_previous, i); if(merge_with_previous) page_scroll -= spacing_y; page_scroll += spacing_y; prev_body_item = items[i].get(); } ++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; BodyItem *prev_body_item; while(num_items_scrolled < selected_int_diff_abs && i >= 0) { if(items[i]->visible) { prev_body_item = get_previous_visible_item(i); const bool merge_with_previous = body_item_merge_handler && body_item_merge_handler(prev_body_item, items[i].get()); page_scroll -= get_item_height(items[i].get(), size.x, selected_int_diff_abs < 50, true, merge_with_previous, i); if(merge_with_previous) page_scroll += spacing_y; page_scroll -= spacing_y; } ++num_items_scrolled; --i; } prev_selected_item = selected_item; } bool merge_with_previous = false; merge_with_previous = body_item_merge_handler && body_item_merge_handler(get_previous_visible_item(selected_item), items[selected_item].get()); selected_item_height = get_item_height(items[selected_item].get(), size.x, true, true, merge_with_previous, selected_item); selected_item_height += spacing_y; bool selected_item_fits_on_screen = selected_item_height <= size.y; selected_line_top_visible = pos.y - start_y + page_scroll >= 0.0f; selected_line_bottom_visible = pos.y - start_y + page_scroll + selected_item_height <= size.y; if(keep_selected_inside_body) { if(pos.y - start_y + page_scroll >= size.y && !selected_item_fits_on_screen) page_scroll = 0.0f; else if(pos.y - start_y + page_scroll + selected_item_height <= 0.0f && !selected_item_fits_on_screen) page_scroll = size.y - selected_item_height; } selected_line_top_visible |= selected_item_fits_on_screen; selected_line_bottom_visible |= selected_item_fits_on_screen; if(keep_selected_inside_body) { if(!first_item_fully_visible && !last_item_fully_visible) { if(offset_to_top > 0.0f) page_scroll -= offset_to_top; else if(offset_to_bottom > 0.0f) page_scroll += offset_to_bottom; } else { if((attach_side == AttachSide::TOP && first_item_fully_visible)/* || (first_item_fully_visible && !last_item_fully_visible)*/) page_scroll -= offset_to_top; else if((attach_side == AttachSide::BOTTOM && last_item_fully_visible) || (last_item_fully_visible && !first_item_fully_visible)) page_scroll += offset_to_bottom; } if(page_scroll > size.y - selected_item_height && selected_item_fits_on_screen) { page_scroll = size.y - selected_item_height; if(merge_with_previous) page_scroll += spacing_y*2.0f; BodyItem *next_body_item = get_next_visible_item(selected_item); const bool merge_with_next = next_body_item && body_item_merge_handler && body_item_merge_handler(items[selected_item].get(), next_body_item); if(!merge_with_previous && merge_with_next) page_scroll += spacing_y; } else if(page_scroll < (merge_with_previous ? spacing_y : 0.0f) && selected_line_top_visible && selected_item_fits_on_screen) { if(merge_with_previous) page_scroll = spacing_y; else page_scroll = 0.0f; } } //page_scroll = std::floor(page_scroll); pos.y += page_scroll; first_item_fully_visible = true; bool last_item_fully_visible_set = false; bool items_cut_off_set = false; sf::Vector2u window_size = window.getSize(); sf::Vector2f prev_pos = pos; int i; for(i = selected_item - 1; i >= 0; --i) { auto &item = items[i]; // TODO: Find a better solution? if(!item->visible) continue; BodyItem *prev_body_item = get_previous_visible_item(i); const bool merge_with_previous = body_item_merge_handler && body_item_merge_handler(prev_body_item, item.get()); item->last_drawn_time = elapsed_time_sec; float extra_page_scroll = page_scroll; float item_height = get_item_height(item.get(), size.x, true, true, merge_with_previous, i); prev_pos.y += (page_scroll - extra_page_scroll); float item_height_with_merge = item_height; item_height_with_merge += spacing_y; prev_pos.y -= item_height_with_merge; if(prev_pos.y < start_y) { items_cut_off = true; items_cut_off_set = true; first_item_fully_visible = false; } if(prev_pos.y + item_height_with_merge <= 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, true, merge_with_previous); glDisable(GL_SCISSOR_TEST); ++num_visible_items; last_visible_item = i; if(first_item_fully_visible) first_fully_visible_item = i; if(merge_with_previous) prev_pos.y += spacing_y; } offset_to_top = prev_pos.y - start_y; BodyItem *prev_body_item = get_previous_visible_item(selected_item); float prev_item_height = 0.0f; merge_with_previous = false; 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; merge_with_previous = body_item_merge_handler && body_item_merge_handler(prev_body_item, item.get()); if(merge_with_previous) after_pos.y -= spacing_y; float extra_page_scroll = page_scroll; float item_height = get_item_height(item.get(), size.x, true, true, merge_with_previous, i); after_pos.y += (page_scroll - extra_page_scroll); if(after_pos.y < start_y) { items_cut_off = true; first_item_fully_visible = false; } if(after_pos.y - start_y >= size.y) { if(first_fully_visible_item == -1) first_item_fully_visible = false; last_item_fully_visible = false; items_cut_off = true; last_item_fully_visible_set = true; items_cut_off_set = true; break; } item->last_drawn_time = elapsed_time_sec; // 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, true, merge_with_previous); glDisable(GL_SCISSOR_TEST); after_pos.y += item_height; after_pos.y += spacing_y; ++num_visible_items; last_visible_item = i; BodyItem *next_body_item = get_next_visible_item(i); const bool merge_with_next = next_body_item && body_item_merge_handler && body_item_merge_handler(item.get(), next_body_item); if(after_pos.y - start_y - (merge_with_next ? 0.0f : spacing_y) > size.y) { last_item_fully_visible = false; items_cut_off = true; last_item_fully_visible_set = true; items_cut_off_set = true; } else { last_fully_visible_item = i; } if(first_item_fully_visible && first_fully_visible_item == -1) first_fully_visible_item = i; prev_body_item = items[i].get(); prev_item_height = item_height; } if(first_fully_visible_item == -1) first_fully_visible_item = selected_item; if(last_fully_visible_item == -1) last_fully_visible_item = selected_item; offset_to_bottom = size.y - (after_pos.y - start_y - (last_item_fully_visible && merge_with_previous ? spacing_y : 0.0f)); if(!last_item_fully_visible_set) last_item_fully_visible = true; if(!items_cut_off_set) items_cut_off = false; for(auto it = item_thumbnail_textures.begin(); it != item_thumbnail_textures.end();) { if(!it->second->referenced) { it = item_thumbnail_textures.erase(it); loaded_textures_changed = true; } else { ++it; } } // TODO: Only do this for items that are not visible, do not loop all items. // 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_sec - body_item->last_drawn_time >= 1.5) clear_body_item_cache(body_item.get()); // The embedded item might or might not refer to another item in |items|, so we have to make sure we also check it if(body_item->embedded_item && elapsed_time_sec - body_item->embedded_item->last_drawn_time >= 1.5) clear_body_item_cache(body_item->embedded_item.get()); } if(loaded_textures_changed) { loaded_textures_changed = false; malloc_trim(0); } mouse_left_clicked = false; if(clicked_body_item) { auto clicked_body_item_tmp = clicked_body_item; // tmp because below call to body_item_select_callback may call this same draw function clicked_body_item = nullptr; if(body_item_select_callback) body_item_select_callback(clicked_body_item_tmp.get()); } } void Body::update_dirty_state(BodyItem *body_item, float width) { 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().begin(), body_item->get_title().end()); if(body_item->title_text) body_item->title_text->setString(std::move(str)); else body_item->title_text = std::make_unique(std::move(str), false, std::floor(16 * get_ui_scale()), width, title_mark_urls); body_item->title_text->setFillColor(body_item->get_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().begin(), body_item->get_description().end()); if(body_item->description_text) body_item->description_text->setString(std::move(str)); else body_item->description_text = std::make_unique(std::move(str), false, std::floor(14 * get_ui_scale()), width, true); body_item->description_text->setFillColor(body_item->get_description_color()); 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().begin(), body_item->get_author().end()); if(body_item->author_text) body_item->author_text->setString(std::move(str)); else body_item->author_text = std::make_unique(std::move(str), true, std::floor(14 * get_ui_scale()), width); body_item->author_text->setFillColor(body_item->get_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_r(&message_timestamp, &message_tm); //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, "%a %b %d %H:%M", &message_tm); if(body_item->timestamp_text) body_item->timestamp_text->setString(time_str); else body_item->timestamp_text = std::make_unique(time_str, *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(10 * get_ui_scale())); body_item->timestamp_text->setFillColor(sf::Color(185, 190, 198, 100)); } } 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; } } sf::Vector2i Body::get_item_thumbnail_size(BodyItem *item) const { sf::Vector2i content_size; sf::Vector2i thumbnail_max_size_scaled(std::floor(thumbnail_max_size.x * get_ui_scale()), std::floor(thumbnail_max_size.y * get_ui_scale())); if(item->thumbnail_size.x > 0 && item->thumbnail_size.y > 0) content_size = clamp_to_size(sf::Vector2i(std::floor(item->thumbnail_size.x * get_ui_scale()), std::floor(item->thumbnail_size.y * get_ui_scale())), thumbnail_max_size_scaled); else content_size = thumbnail_max_size_scaled; return content_size; } // TODO: Cache, and take into consideration updated items and visibility change BodyItem* Body::get_previous_visible_item(int start_index) { for(int i = start_index - 1; i >= 0; --i) { if(items[i]->visible) return items[i].get(); } return nullptr; } // TODO: Cache, and take into consideration updated items and visibility change BodyItem* Body::get_next_visible_item(int start_index) { for(int i = start_index + 1; i < (int)items.size(); ++i) { if(items[i]->visible) return items[i].get(); } return nullptr; } static sf::Vector2f to_vec2f(const sf::Vector2i &vec) { return sf::Vector2f(vec.x, vec.y); } void Body::draw_item(sf::RenderWindow &window, BodyItem *item, sf::Vector2f pos, sf::Vector2f size, bool include_embedded_item, bool is_embedded) { update_dirty_state(item, size.x); item->last_drawn_time = draw_timer.getElapsedTime().asMilliseconds(); sf::Vector2u window_size = window.getSize(); get_item_height(item, size.x, true, false, false, -1); if(!is_embedded) { 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); if(!is_embedded) { 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(FetchStatus embedded_item_status) { switch(embedded_item_status) { case FetchStatus::NONE: return ""; case FetchStatus::QUEUED_LOADING: case FetchStatus::LOADING: return "Loading message..."; case FetchStatus::FINISHED_LOADING: return "Finished loading message..."; case FetchStatus::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, bool merge_with_previous) { // 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 && !item->thumbnail_url.empty()) { 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 && include_embedded_item) body_item_render_callback(item); sf::Vector2f item_pos; item_pos.x = std::floor(pos.x); item_pos.y = std::floor(pos.y); if(body_item_select_callback && mouse_left_clicked && !clicked_body_item) { sf::FloatRect item_box(pos, sf::Vector2f(size.x, item_height)); // TODO: Scale mouse_press_pixels_moved_abs with monitor PPI instead of using get_ui_scale() if(item_box.contains(mouse_click_pos) && item_box.contains(mouse_release_pos) && mouse_press_pixels_moved_abs <= 50.0 * get_ui_scale()) { clicked_body_item = items[item_index]; set_selected_item(item_index, false); } } //item_separator.setSize(sf::Vector2f(std::max(0.0f, size.x - 20.0f), 1.0f)); //item_separator.setPosition(item_pos + sf::Vector2f(10.0f, std::floor(item_height + spacing_y * 0.5f))); //window.draw(item_separator); if(item_index == selected_item) { item_background.set_position(item_pos); item_background.set_size(sf::Vector2f(size.x, item_height)); item_background.draw(window); } float text_offset_x = padding_x; if(draw_thumbnails && item_thumbnail && !merge_with_previous) { double elapsed_time_thumbnail = 0.0; if(item_thumbnail->loading_state == LoadingState::APPLIED_TO_TEXTURE) elapsed_time_thumbnail = item_thumbnail->texture_applied_time.getElapsedTime().asSeconds(); //thumbnail_fade_duration_sec bool only_show_thumbnail = false; double thumbnail_fade_progress = elapsed_time_thumbnail / thumbnail_fade_duration_sec; if(thumbnail_fade_progress > 1.0) { thumbnail_fade_progress = 1.0; only_show_thumbnail = true; } // TODO: Verify if this is safe. The thumbnail is being modified in another thread 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(); sf::Vector2f image_size_f(image_size.x, image_size.y); sf::Vector2f content_size = to_vec2f(get_item_thumbnail_size(item)); auto new_image_size = clamp_to_size(image_size_f, content_size); auto image_scale = get_ratio(image_size_f, new_image_size); image.setColor(sf::Color(255, 255, 255, thumbnail_fade_progress * 255)); image.setScale(image_scale); image.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y)); if(thumbnail_mask_shader && item->thumbnail_mask_type == ThumbnailMaskType::CIRCLE) { thumbnail_mask_shader->setUniform("resolution", new_image_size); window.draw(image, thumbnail_mask_shader); } else { window.draw(image); } text_offset_x += image_padding_x + new_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() && !only_show_thumbnail) { sf::Vector2f content_size = to_vec2f(get_item_thumbnail_size(item)); sf::Uint8 fallback_fade_alpha = (1.0 - thumbnail_fade_progress) * 255; sf::Color fallback_color(52, 58, 70, fallback_fade_alpha); if(thumbnail_mask_shader && item->thumbnail_mask_type == ThumbnailMaskType::CIRCLE) { // TODO: Use the mask shader instead, but a vertex shader is also needed for that to pass the vertex coordinates since // shapes dont have texture coordinates. // TODO: Cache circle shape sf::CircleShape circle_shape(content_size.x * 0.5f); circle_shape.setFillColor(fallback_color); circle_shape.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y)); window.draw(circle_shape); } else { image_fallback.setSize(content_size); image_fallback.setFillColor(fallback_color); image_fallback.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y)); window.draw(image_fallback); } sf::Vector2f loading_icon_size(loading_icon.getTexture()->getSize().x, loading_icon.getTexture()->getSize().y); auto new_loading_icon_size = clamp_to_size(loading_icon_size, content_size); loading_icon.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y) + (content_size * 0.5f)); loading_icon.setScale(get_ratio(loading_icon_size, new_loading_icon_size)); loading_icon.setRotation(elapsed_time_sec * 400.0); loading_icon.setColor(sf::Color(255, 255, 255, fallback_fade_alpha)); window.draw(loading_icon); text_offset_x += image_padding_x + content_size.x; } } else if(item->thumbnail_size.x > 0) { text_offset_x += image_padding_x + get_item_thumbnail_size(item).x; } const float timestamp_text_y = std::floor(item_pos.y + padding_y - std::floor(6.0f * get_ui_scale())); if(item->author_text && !merge_with_previous) { item->author_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 6.0f * get_ui_scale())); 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 + std::floor(3.0f * get_ui_scale()); } if(include_embedded_item && item->embedded_item_status != FetchStatus::NONE) { const float border_width = 4.0f; const float embedded_item_width = std::floor(size.x - text_offset_x - border_width - padding_x); float embedded_item_height = item->embedded_item ? get_item_height(item->embedded_item.get(), embedded_item_width, true, false) : ((embedded_item_font_size + 5.0f) + embedded_item_padding_y * 2.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(embedded_item_width, embedded_item_height); draw_item(window, item->embedded_item.get(), embedded_item_pos, embedded_item_size, false, true); } 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_font_size + 5.0f) * 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->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - std::floor(6.0f * get_ui_scale()))); 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 + std::floor(3.0f * get_ui_scale()); } 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 - std::floor(6.0f * get_ui_scale()) + 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->reactions.empty() && include_embedded_item) { float reaction_offset_x = 0.0f; item_pos.y += reaction_padding_y; float reaction_max_height = 0.0f; // TODO: Fix first row wrap-around for(int i = 0; i < item->reactions.size(); ++i) { auto &reaction = item->reactions[i]; reaction.text->setMaxWidth(size.x - text_offset_x - image_padding_x); reaction.text->updateGeometry(); reaction_max_height = std::max(reaction_max_height, reaction.text->getHeight()); reaction.text->setPosition(std::floor(item_pos.x + text_offset_x + reaction_offset_x + reaction_background_padding_x), std::floor(item_pos.y + padding_y - 4.0f + reaction_background_padding_y)); reaction_background.set_position(sf::Vector2f(std::floor(item_pos.x + text_offset_x + reaction_offset_x), std::floor(item_pos.y + padding_y))); reaction_background.set_size(sf::Vector2f(reaction.text->getWidth() + reaction_background_padding_x * 2.0f, reaction.text->getHeight() + reaction_background_padding_y * 2.0f)); reaction_background.draw(window); reaction_offset_x += reaction.text->getWidth() + reaction_background_padding_x * 2.0f + reaction_spacing_x; reaction.text->draw(window); if(text_offset_x + reaction_offset_x + reaction.text->getWidth() + reaction_background_padding_x * 2.0f > size.x && i < (int)item->reactions.size() - 1) { reaction_offset_x = 0.0f; item_pos.y += reaction.text->getHeight() + reaction_padding_y + std::floor(6.0f * get_ui_scale()); reaction_max_height = reaction.text->getHeight(); } } item_pos.y += reaction_max_height + reaction_padding_y; } if(item_index == selected_item && item->timestamp_text) { item->timestamp_text->setPosition(std::floor(item_pos.x + size.x - item->timestamp_text->getLocalBounds().width - padding_x), timestamp_text_y + 8.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 + std::floor(6.0f * get_ui_scale())); window.draw(progress_text); } } } float Body::get_item_height(BodyItem *item, float width, bool load_texture, bool include_embedded_item, bool merge_with_previous, int item_index) { float image_height = 0.0f; float text_offset_x = padding_x; if(draw_thumbnails && !item->thumbnail_url.empty() && !merge_with_previous) { sf::Vector2i content_size = get_item_thumbnail_size(item); image_height = content_size.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) { item_thumbnail->referenced = true; if(!item->thumbnail_url.empty() && item_thumbnail->loading_state == LoadingState::NOT_LOADED) AsyncImageLoader::get_instance().load_thumbnail(item->thumbnail_url, item->thumbnail_is_local, content_size, 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; item_thumbnail->texture_applied_time.restart(); loaded_textures_changed = true; } } if(item_thumbnail && item_thumbnail->loading_state == LoadingState::APPLIED_TO_TEXTURE && item_thumbnail->texture.getNativeHandle() != 0) { auto image_size = item_thumbnail->texture.getSize(); sf::Vector2f image_size_f(image_size.x, image_size.y); auto new_image_size = clamp_to_size(image_size_f, to_vec2f(content_size)); image_height = new_image_size.y; text_offset_x += image_padding_x + new_image_size.x; } else { text_offset_x += image_padding_x + content_size.x; } } else if(item->thumbnail_size.x > 0) { text_offset_x += image_padding_x + get_item_thumbnail_size(item).x; } if(load_texture) update_dirty_state(item, width - text_offset_x - image_padding_x); float item_height = 0.0f; if(item->title_text) { item_height += item->title_text->getHeight() - 2.0f + std::floor(3.0f * get_ui_scale()); } if(item->author_text && !merge_with_previous) { item_height += item->author_text->getHeight() - 2.0f + std::floor(3.0f * get_ui_scale()); } if(include_embedded_item && item->embedded_item_status != FetchStatus::NONE) { if(item->embedded_item) item_height += (get_item_height(item->embedded_item.get(), width, load_texture, false) + 4.0f + embedded_item_padding_y * 2.0f); else item_height += ((embedded_item_font_size + 5.0f) + 4.0f + embedded_item_padding_y * 2.0f); } if(item->description_text) { item_height += item->description_text->getHeight() - 2.0f; } if(!item->reactions.empty() && include_embedded_item) { float reaction_offset_x = 0.0f; item_height += reaction_padding_y; float reaction_max_height = 0.0f; for(int i = 0; i < item->reactions.size(); ++i) { auto &reaction = item->reactions[i]; reaction.text->setMaxWidth(width - text_offset_x - image_padding_x); reaction.text->updateGeometry(); reaction_max_height = std::max(reaction_max_height, reaction.text->getHeight()); reaction_offset_x += reaction.text->getWidth() + reaction_background_padding_x * 2.0f + reaction_spacing_x; if(text_offset_x + reaction_offset_x + reaction.text->getWidth() + reaction_background_padding_x * 2.0f > width && i < (int)item->reactions.size() - 1) { reaction_offset_x = 0.0f; item_height += reaction.text->getHeight() + reaction_padding_y + std::floor(6.0f * get_ui_scale()); reaction_max_height = reaction.text->getHeight(); } } item_height += reaction_max_height + reaction_padding_y; } item_height = std::max(item_height, image_height); item_height += (padding_y * 2.0f); if(attach_side == AttachSide::TOP && item_index != -1 && item_index < selected_item && item->calculated_height >= 0.0f) page_scroll += (item_height - item->calculated_height); item->calculated_height = item_height; return item_height; } float Body::get_spacing_y() const { return spacing_y; } static size_t str_find_case_insensitive(const std::string &str, size_t start_index, const char *substr, size_t substr_len) { auto it = std::search(str.begin() + start_index, str.end(), substr, substr + substr_len, [](char c1, char c2) { return std::toupper(c1) == std::toupper(c2); }); if(it == str.end()) return std::string::npos; return it - str.begin(); } // TODO: Support utf-8 case insensitive find static bool string_find_fuzzy_case_insensitive(const std::string &str, const std::string &substr) { if(str.empty()) return false; if(substr.empty()) return true; size_t str_index = 0; bool full_match = true; string_split(substr, ' ', [&str, &str_index, &full_match](const char *str_part, size_t size) { if(size == 0) return true; size_t found_index = str_find_case_insensitive(str, str_index, str_part, size); if(found_index == std::string::npos) { full_match = false; return false; } str_index = found_index + size; return true; }); return full_match; } void Body::filter_search_fuzzy(const std::string &text) { if(text.empty()) { for(auto &item : items) { item->visible = true; } return; } for(auto &item : items) { filter_search_fuzzy_item(text, item.get()); } select_first_item(); } void Body::filter_search_fuzzy_item(const std::string &text, BodyItem *body_item) { body_item->visible = string_find_fuzzy_case_insensitive(body_item->get_title(), text); if(!body_item->visible && !body_item->get_description().empty()) body_item->visible = string_find_fuzzy_case_insensitive(body_item->get_description(), text); if(!body_item->visible && !body_item->get_author().empty()) body_item->visible = string_find_fuzzy_case_insensitive(body_item->get_author(), text); } 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; } void Body::set_page_scroll(float scroll) { selected_scrolled = 0.0f; page_scroll = scroll; clamp_selected_item_to_body_count = 1; } }