#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/Theme.hpp" #include "../include/StringUtils.hpp" #include "../plugins/Plugin.hpp" #include #include #include #include #include struct BodySpacing { float spacing_y = 0.0f; float padding_x = 0.0f; float image_padding_x = 0.0f; float padding_y = 0.0f; float padding_y_text_only = 0.0f; float embedded_item_padding_y = 0.0f; float body_padding_horizontal = 0.0f; float body_padding_vertical = 0.0f; float reaction_background_padding_x = 0.0f; float reaction_background_padding_y = 0.0f; float reaction_spacing_x = 0.0f; float reaction_padding_y = 0.0f; float embedded_item_font_size = 0.0f; }; static const int card_width = 250.0f * QuickMedia::get_ui_scale(); static const int card_height = 350.0f * QuickMedia::get_ui_scale(); static const int min_column_spacing = 10 * QuickMedia::get_ui_scale(); static const int card_padding_x = 20 * QuickMedia::get_ui_scale(); static const int card_padding_y = 20 * QuickMedia::get_ui_scale(); static const int card_image_text_padding = 10 * QuickMedia::get_ui_scale(); static const sf::Vector2i card_max_image_size(card_width - card_padding_x * 2, (card_height - card_padding_y * 2) / 2); static const int num_columns_switch_to_list = 1; static const int embedded_item_border_width = 4; static BodySpacing body_spacing[2]; static bool themes_initialized = false; namespace QuickMedia { static void init_body_theme_minimal() { body_spacing[BODY_THEME_MINIMAL].spacing_y = std::floor(10.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MINIMAL].padding_x = std::floor(10.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MINIMAL].image_padding_x = std::floor(5.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MINIMAL].padding_y = std::floor(5.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MINIMAL].padding_y_text_only = std::floor(5.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MINIMAL].embedded_item_padding_y = std::floor(0.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MINIMAL].body_padding_horizontal = std::floor(10.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MINIMAL].body_padding_vertical = std::floor(10.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MINIMAL].reaction_background_padding_x = std::floor(7.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MINIMAL].reaction_background_padding_y = std::floor(3.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MINIMAL].reaction_spacing_x = std::floor(5.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MINIMAL].reaction_padding_y = std::floor(7.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MINIMAL].embedded_item_font_size = std::floor(14 * QuickMedia::get_ui_scale()); } static void init_body_theme_modern_spacious() { body_spacing[BODY_THEME_MODERN_SPACIOUS].spacing_y = std::floor(20.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MODERN_SPACIOUS].padding_x = std::floor(20.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MODERN_SPACIOUS].image_padding_x = std::floor(15.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MODERN_SPACIOUS].padding_y = std::floor(15.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MODERN_SPACIOUS].padding_y_text_only = std::floor(7.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MODERN_SPACIOUS].embedded_item_padding_y = std::floor(0.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MODERN_SPACIOUS].body_padding_horizontal = std::floor(20.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MODERN_SPACIOUS].body_padding_vertical = std::floor(20.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_background_padding_x = std::floor(7.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_background_padding_y = std::floor(3.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_spacing_x = std::floor(5.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_padding_y = std::floor(7.0f * QuickMedia::get_ui_scale()); body_spacing[BODY_THEME_MODERN_SPACIOUS].embedded_item_font_size = std::floor(14 * QuickMedia::get_ui_scale()); } static void init_body_themes() { if(themes_initialized) return; init_body_theme_minimal(); init_body_theme_modern_spacious(); themes_initialized = true; } static sf::Vector2f to_vec2f(const sf::Vector2i &vec) { return sf::Vector2f(vec.x, vec.y); } Body::Body(BodyTheme body_theme, sf::Texture &loading_icon_texture, sf::Shader *rounded_rectangle_shader, sf::Shader *rounded_rectangle_mask_shader) : draw_thumbnails(true), body_item_render_callback(nullptr), thumbnail_mask_shader(nullptr), body_theme(body_theme), selected_item(0), prev_selected_item(0), loading_icon(loading_icon_texture), 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())), num_visible_items(0), top_cut_off(false), bottom_cut_off(false), item_background(sf::Vector2f(1.0f, 1.0f), 10.0f, get_current_theme().selected_color, rounded_rectangle_shader), reaction_background(sf::Vector2f(1.0f, 1.0f), 10.0f, get_current_theme().shade_color, rounded_rectangle_shader), rounded_rectangle_mask_shader(rounded_rectangle_mask_shader) { assert(rounded_rectangle_shader); assert(rounded_rectangle_mask_shader); init_body_themes(); embedded_item_load_text = sf::Text("", *FontLoader::get_font(FontLoader::FontType::LATIN), body_spacing[body_theme].embedded_item_font_size); progress_text.setFillColor(get_current_theme().text_color); replies_text.setFillColor(get_current_theme().replies_text_color); thumbnail_max_size.x = 600; thumbnail_max_size.y = 337; 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); render_selected_item_bg = !is_touch_enabled(); } Body::~Body() { 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_item_fits_in_body) return select_previous_item(false, true); for(int i = 0; i < num_visible_items - 1; ++i) { if(!select_previous_item(false, true)) 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_item_fits_in_body) return select_next_item(false, true); for(int i = 0; i < num_visible_items - 1; ++i) { if(!select_next_item(false, true)) return false; } return true; } bool Body::select_previous_item(bool scroll_page_if_large_item, bool ignore_columns) { if(items.empty()) return false; if(num_columns == 1 && scroll_page_if_large_item && !selected_item_fits_in_body && !selected_line_top_visible) { selected_scrolled += 128.0f; return true; } int new_selected_item = selected_item; for(int i = 0; i < (ignore_columns ? 1 : num_columns); ++i) { const int prev = new_selected_item; new_selected_item = get_previous_visible_item(new_selected_item); if(new_selected_item == -1) { if(i == 0) return false; new_selected_item = prev; break; } } selected_item = new_selected_item; return true; } bool Body::select_next_item(bool scroll_page_if_large_item, bool ignore_columns) { if(items.empty()) return false; if(num_columns == 1 && scroll_page_if_large_item && !selected_item_fits_in_body && !selected_line_bottom_visible) { selected_scrolled -= 128.0f; return true; } int new_selected_item = selected_item; for(int i = 0; i < (ignore_columns ? 1 : num_columns); ++i) { const int prev = new_selected_item; new_selected_item = get_next_visible_item(new_selected_item); if(new_selected_item == -1) { if(i == 0) return false; new_selected_item = prev; break; } } 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()); selected_item = item; clamp_selection(); //if(reset_prev_selected_item) // prev_selected_item = selected_item; //page_scroll = 0.0f; } void Body::set_selected_item(BodyItem *body_item) { if(!body_item) return; for(size_t i = 0; i < items.size(); ++i) { if(items[i].get() == body_item) { selected_item = i; break; } } clamp_selection(); } 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(bool reset_page_scroll) { selected_item = 0; if(attach_side == AttachSide::TOP && reset_page_scroll) { prev_selected_item = selected_item; page_scroll = 0.0f; } clamp_selection(); } void Body::select_last_item() { int new_selected_item = std::max(0, (int)items.size() - 1); selected_item = new_selected_item; //page_scroll = 0.0f; clamp_selection(); } void Body::set_items(BodyItems items) { for(auto &item : items) { filter_search_fuzzy_item(current_filter, item.get()); } this->items = std::move(items); if(attach_side == AttachSide::TOP) { selected_item = 0; prev_selected_item = selected_item; page_scroll = 0.0f; } } void Body::clear_items() { items.clear(); selected_item = 0; prev_selected_item = selected_item; page_scroll = 0.0f; } void Body::prepend_item(std::shared_ptr body_item) { apply_search_filter_for_item(body_item.get()); items.insert(items.begin(), std::move(body_item)); } void Body::prepend_items_reverse(BodyItems new_items) { for(auto &item : new_items) { filter_search_fuzzy_item(current_filter, item.get()); } items.insert(items.begin(), std::make_move_iterator(new_items.rbegin()), std::make_move_iterator(new_items.rend())); } void Body::append_item(std::shared_ptr body_item) { apply_search_filter_for_item(body_item.get()); items.push_back(std::move(body_item)); } void Body::append_items(BodyItems new_items) { for(auto &item : new_items) { filter_search_fuzzy_item(current_filter, item.get()); } items.insert(items.end(), std::make_move_iterator(new_items.begin()), std::make_move_iterator(new_items.end())); } void Body::insert_item(std::shared_ptr body_item, int index) { apply_search_filter_for_item(body_item.get()); items.insert(items.begin() + index, std::move(body_item)); } void Body::move_item(size_t src_index, size_t dst_index) { assert(src_index < items.size()); assert(dst_index < items.size()); auto item_to_move = std::move(items[src_index]); items.erase(items.begin() + src_index); if(dst_index <= src_index) items.insert(items.begin() + dst_index, std::move(item_to_move)); else items.insert(items.begin() + dst_index - 1, std::move(item_to_move)); } // TODO: Binary search and use hint to start search from start or end (for example when adding "previous" items or "next" items) size_t Body::insert_item_by_timestamp(std::shared_ptr body_item) { apply_search_filter_for_item(body_item.get()); 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 i; } } items.push_back(std::move(body_item)); return items.size() - 1; } // TODO: Optimize by resizing |items| before insert void Body::insert_items_by_timestamps(BodyItems new_items) { BodyItem *selected_body_item = nullptr; if(selected_item >= 0 && selected_item < (int)items.size()) selected_body_item = items[selected_item].get(); for(auto &new_item : new_items) { insert_item_by_timestamp(new_item); } clamp_selection(); if(!selected_body_item) return; for(size_t i = 0; i < items.size(); ++i) { if(selected_body_item == items[i].get()) { set_selected_item(i); break; } } } void Body::for_each_item(std::function&)> callback) { for(std::shared_ptr &body_item : items) { callback(body_item); } } std::shared_ptr Body::find_item(std::function&)> callback) { for(std::shared_ptr &body_item : items) { if(callback(body_item)) return body_item; } return nullptr; } int Body::find_item_index(std::function&)> callback) { for(int i = 0; i != (int)items.size(); ++i) { std::shared_ptr &body_item = items[i]; if(callback(body_item)) return i; } return -1; } bool Body::erase_item(std::function&)> callback) { for(auto it = items.begin(), end = items.end(); it != end; ++it) { if(callback(*it)) { items.erase(it); return true; } } return false; } std::shared_ptr Body::get_item_by_index(size_t index) { assert(index < items.size()); return items[index]; } BodyItemList Body::get_items() { return BodyItemList(&items); } BodyItems Body::get_items_copy() { return items; } void Body::copy_range(size_t start_index, size_t end_index, BodyItems &target) { assert(end_index == (size_t)-1 || end_index >= start_index); assert(start_index < items.size() && (end_index == (size_t)-1 || end_index < items.size())); target.insert(target.end(), items.begin() + start_index, end_index == (size_t)-1 ? items.end() : (items.begin() + end_index)); } size_t Body::get_num_items() const { return items.size(); } void Body::reverse_items() { std::reverse(items.begin(), items.end()); } void Body::clear_cache() { clear_text_cache(); 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()); } } 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]; } void Body::clamp_selection() { int num_items = (int)items.size(); if(items.empty()) { selected_item = 0; 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, bool keyboard_navigation) { if(keyboard_navigation && event.type == sf::Event::KeyPressed && !event.key.alt) { const bool rendering_card_view = card_view && card_view_enabled; if(event.key.code == sf::Keyboard::Up || (event.key.control && event.key.code == sf::Keyboard::K)) { render_selected_item_bg = true; bool top_reached = select_previous_item(true); if(!top_reached && on_top_reached) on_top_reached(); return true; } else if(event.key.code == sf::Keyboard::Down || (event.key.control && event.key.code == sf::Keyboard::J)) { render_selected_item_bg = true; bool bottom_reached = select_next_item(true); if(!bottom_reached && on_bottom_reached) on_bottom_reached(); return true; } else if(rendering_card_view && selected_column > 0 && ((!event.key.control && event.key.code == sf::Keyboard::Left) || (event.key.control && event.key.code == sf::Keyboard::H))) { render_selected_item_bg = true; const int new_selected_item = get_previous_visible_item(selected_item); if(new_selected_item != -1) { selected_item = new_selected_item; } else if(on_top_reached) { on_top_reached(); } return true; } else if(rendering_card_view && selected_column + 1 < num_columns && ((!event.key.control && event.key.code == sf::Keyboard::Right) || (event.key.control && event.key.code == sf::Keyboard::L))) { render_selected_item_bg = true; const int new_selected_item = get_next_visible_item(selected_item); if(new_selected_item != -1) { selected_item = new_selected_item; } else if(on_bottom_reached) { on_bottom_reached(); } return true; } else if(event.key.code == sf::Keyboard::Home) { render_selected_item_bg = true; select_first_item(false); if(on_top_reached) on_top_reached(); return true; } else if(event.key.code == sf::Keyboard::End) { render_selected_item_bg = true; select_last_item(); if(on_bottom_reached) on_bottom_reached(); return true; } else if(event.key.code == sf::Keyboard::PageUp) { render_selected_item_bg = true; bool top_reached = select_previous_page(); if(!top_reached && on_top_reached) on_top_reached(); return true; } else if(event.key.code == sf::Keyboard::PageDown) { render_selected_item_bg = true; bool bottom_reached = select_next_page(); if(!bottom_reached && on_bottom_reached) on_bottom_reached(); return true; } } 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_raw = mouse_pos_raw; mouse_click_pos = mouse_pos; mouse_press_pixels_moved_abs = 0.0; has_scrolled_with_input = true; render_selected_item_bg = false; click_counts = std::abs(mouse_scroll_accel.y) < 5.0f; // Touching the body while it scrolls should stop it, not select the touched item body_swipe_x = 0.0; body_swipe_move_right = false; grabbed_left_side = (event.mouseButton.x < body_pos.x + body_size.x * 0.15f); 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); if(event.mouseButton.x > body_pos.x + body_size.x * 0.5f || mouse_scroll_accel.x > 20.0f) body_swipe_move_right = true; 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; render_selected_item_bg = false; return true; } return false; } void Body::draw_drop_shadow(sf::RenderWindow &window) { if(!show_drop_shadow) return; const sf::Color color(0, 0, 0, 50); const float height = 5.0f; sf::Vertex gradient_points[4]; gradient_points[0] = sf::Vertex(body_pos + sf::Vector2f(0.0f, 0.0f), color); gradient_points[1] = sf::Vertex(body_pos + sf::Vector2f(body_size.x, 0.0f), color); gradient_points[2] = sf::Vertex(body_pos + sf::Vector2f(body_size.x, height), sf::Color(color.r, color.g, color.b, 0)); gradient_points[3] = sf::Vertex(body_pos + sf::Vector2f(0.0f, height), sf::Color(color.r, color.g, color.b, 0)); window.draw(gradient_points, 4, sf::Quads); } 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) { const bool rendering_card_view = card_view && card_view_enabled; body_size_changed = std::abs(size.x - body_size.x) > 0.1f || std::abs(size.y - body_size.y) > 0.1f; if(body_size_changed) body_size = size; body_pos = pos; const float scissor_y = pos.y; pos.y = 0.0f; if(!rendering_card_view) pos.x += body_spacing[body_theme].body_padding_horizontal; if(attach_side == AttachSide::TOP) pos.y += body_spacing[body_theme].body_padding_vertical; if(!rendering_card_view) size.x = std::max(0.0f, size.x - body_spacing[body_theme].body_padding_horizontal * 2.0f); float frame_time = frame_timer.restart().asSeconds(); if(frame_time > 1.0f) frame_time = 1.0f; // TODO: Remove the need for this. This is needed because fps is changed to 20 or 2 when idle. That idle handler should be removed //if(frame_time > 0.01666f) // frame_time = 0.01666f; if(selected_item >= (int)items.size()) selected_item = (int)items.size() - 1; if(selected_item < 0) selected_item = 0; if(prev_selected_item < 0 || prev_selected_item >= (int)items.size()) { prev_selected_item = selected_item; } elapsed_time_sec = draw_timer.getElapsedTime().asSeconds(); const int prev_num_visible_items = num_visible_items; const bool prev_items_cut_off = top_cut_off || bottom_cut_off; const int prev_first_visible_item = first_visible_item; const int prev_last_visible_item = last_visible_item; num_visible_items = 0; first_visible_item = -1; last_visible_item = -1; selected_line_top_visible = true; selected_line_bottom_visible = true; selected_item_fits_in_body = true; top_cut_off = false; bottom_cut_off = false; num_columns = 1; selected_column = 0; const int selected_item_diff = selected_item - prev_selected_item; prev_selected_item = selected_item; const int num_items = items.size(); if(num_items == 0 || size.x <= 0.001f || size.y <= 0.001f) { if(num_items == 0) page_scroll = 0.0f; 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()); } mouse_left_clicked = false; clicked_body_item = nullptr; draw_drop_shadow(window); return; } if(is_touch_enabled()) { const sf::Vector2f mouse_pos_diff(mouse_pos_raw.x - mouse_pos.x, mouse_pos_raw.y - mouse_pos.y); const float move_speed = 35.0f; sf::Vector2f prev_mouse_pos = mouse_pos; mouse_pos.x += (mouse_pos_diff.x * std::min(1.0f, frame_time * move_speed)); mouse_pos.y += (mouse_pos_diff.y * std::min(1.0f, frame_time * move_speed)); const sf::Vector2f mouse_pos_diff_smooth = mouse_pos - prev_mouse_pos; sf::Vector2f mouse_pos_raw_diff(mouse_pos_raw.x - prev_mouse_pos_raw.x, mouse_pos_raw.y - prev_mouse_pos_raw.y); prev_mouse_pos_raw = mouse_pos_raw; if(prev_items_cut_off) { if(mouse_left_pressed) { body_swipe_x += mouse_pos_diff_smooth.x; if(body_swipe_x < 0.0) body_swipe_x = 0.0; page_scroll += mouse_pos_diff_smooth.y; mouse_scroll_accel = sf::Vector2f(mouse_pos_raw_diff.x, mouse_pos_raw_diff.y) / (frame_time * 120.0f); } else { page_scroll += mouse_scroll_accel.y; } } if(mouse_scroll_accel.y > 0.1f && prev_first_visible_item != -1) { selected_item = prev_first_visible_item; // TODO: Cache this if(on_top_reached && get_previous_visible_item(selected_item) == -1) on_top_reached(); } else if(mouse_scroll_accel.y < -0.1f && prev_last_visible_item != -1) { selected_item = prev_last_visible_item; // TODO: Cache this if(on_bottom_reached && get_next_visible_item(selected_item) == -1) on_bottom_reached(); } if(!mouse_left_pressed) { const float scroll_deaccel = 1.02f; double deaccel = scroll_deaccel * (1.0 + frame_time); if(deaccel > 0.0001) { 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; } else { deaccel = 0.0; mouse_scroll_accel.x = 0.0; mouse_scroll_accel.y = 0.0; } double dist_to_target; if(body_swipe_move_right) dist_to_target = (body_size.x - body_spacing[body_theme].body_padding_horizontal) - body_swipe_x; else dist_to_target = 0.0f - body_swipe_x; body_swipe_x += (dist_to_target * std::min(1.0f, frame_time * move_speed)); } } if(swiping_enabled && grabbed_left_side) pos.x += body_swipe_x; if(!rendering_card_view) { const int selected_prev_item = get_previous_visible_item(selected_item); const bool selected_merge_with_previous = selected_prev_item != -1 && body_item_merge_handler && body_item_merge_handler(items[selected_prev_item].get(), items[selected_item].get()); get_item_height(items[selected_item].get(), size.x, true, true, selected_merge_with_previous, selected_item); selected_item_fits_in_body = items[selected_item]->loaded_height < size.y; if(selected_item_fits_in_body) selected_scrolled = 0.0f; } const sf::Vector2u window_size = window.getSize(); const sf::View prev_view = window.getView(); if(!rendering_card_view) { sf::View new_view(sf::FloatRect(0.0f, 0.0f, window_size.x, size.y)); new_view.setViewport(sf::FloatRect(0.0f, scissor_y / (float)window_size.y, 1.0f, size.y / (float)window_size.y)); window.setView(new_view); } bool instant_move = body_size_changed; if(target_set == TargetSetState::SET) { target_set = TargetSetState::APPLIED; instant_move = true; } const float speed = 30.0f; const sf::Vector2f item_background_size_diff = sf::Vector2f(item_background_target_size.x, item_background_target_height) - item_background_prev_size; const float item_background_size_speed = instant_move ? 1000.0f : speed; const sf::Vector2f item_background_new_size = item_background_prev_size + (item_background_size_diff * std::min(1.0f, frame_time * item_background_size_speed)); item_background_prev_size = item_background_new_size; const sf::Vector2f item_background_pos_diff = item_background_target_pos - item_background_prev_pos; const float item_background_move_speed = instant_move ? 1000.0f : speed; sf::Vector2f item_background_new_pos = item_background_prev_pos + (item_background_pos_diff * std::min(1.0f, frame_time * item_background_move_speed)); if(selected_item_fits_in_body) { item_background_new_pos.y = std::min(item_background_new_pos.y, size.y - item_background_new_size.y); item_background_new_pos.y = std::max(item_background_new_pos.y, 0.0f); } item_background_prev_pos = item_background_new_pos; float body_total_height = 0.0f; if(rendering_card_view) { draw_card_view(window, pos, size, window_size, scissor_y); } else { body_total_height = draw_list_view(window, pos, size, prev_num_visible_items, content_progress); } window.setView(prev_view); draw_drop_shadow(window); // TODO: Move up where scroll is limited? then we wont delay this by 1 frame creating a small scroll overflow only in the opposite direction of attach side. // Also take |selected_scrolled| into consideration // Limit scroll in the opposide direction of attach side, since the scroll is already limited for the attach side above (with a simple check) if(!rendering_card_view && (body_size_changed || is_touch_enabled())) { if(attach_side == AttachSide::TOP) { if(top_cut_off && !bottom_cut_off && body_total_height > (size.y - body_spacing[body_theme].body_padding_vertical)) page_scroll = -(body_total_height - (size.y - body_spacing[body_theme].body_padding_vertical)); } else { body_total_height = -body_total_height; if(bottom_cut_off && !top_cut_off && body_total_height > size.y) page_scroll = (body_total_height - size.y); } } 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()); } if(is_touch_enabled()) return; const float item_target_top_diff = item_background_target_pos.y - selected_scrolled - (attach_side == AttachSide::TOP ? body_spacing[body_theme].body_padding_vertical : 0.0f); const float item_target_bottom_diff = (item_background_target_pos.y - selected_scrolled + item_background_target_size.y + body_spacing[body_theme].spacing_y) - size.y; if(item_target_top_diff < 0.0f || !selected_item_fits_in_body) { //extra_scroll_target -= item_target_top_diff; stuck_direction = StuckDirection::TOP; } else if(item_target_bottom_diff > 0.0) { //extra_scroll_target -= item_target_bottom_diff; stuck_direction = StuckDirection::BOTTOM; } if(stuck_direction == StuckDirection::TOP && selected_item_diff > 0 && item_target_top_diff > -0.0001f/* && scroll_diff > -0.0001f*/) stuck_direction = StuckDirection::NONE; if(stuck_direction == StuckDirection::BOTTOM && selected_item_diff < 0 && item_target_bottom_diff < 0.0001f/* && scroll_diff < 0.0001f*/) stuck_direction = StuckDirection::NONE; const float page_scroll_speed = std::min(1.0f, frame_time * speed); if(stuck_direction == StuckDirection::TOP) { if(body_size_changed) page_scroll -= item_target_top_diff; else page_scroll -= item_target_top_diff*page_scroll_speed; } if(stuck_direction == StuckDirection::BOTTOM) { if(body_size_changed) page_scroll -= item_target_bottom_diff; else page_scroll -= item_target_bottom_diff*page_scroll_speed; } } void Body::update_dirty_state(BodyItem *body_item, float width) { if((body_item->dirty && !body_item->get_title().empty()) || (body_size_changed && body_item->title_text)) { 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)); body_item->title_text->setMaxWidth(width); } 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->get_description().empty()) || (body_size_changed && body_item->description_text)) { 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)); body_item->description_text->setMaxWidth(width); } 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->get_author().empty()) || (body_size_changed && body_item->author_text)) { 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)); body_item->author_text->setMaxWidth(width); } 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->get_timestamp() != 0) || (body_size_changed && body_item->timestamp_text)) { body_item->dirty_timestamp = false; if(body_item->get_timestamp() != 0) { //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}; strftime(time_str, sizeof(time_str) - 1, "%Y %b %d, %a %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(get_current_theme().timestamp_text_color); } } } 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; } body_item->keep_alive_frames = 0; } sf::Vector2i Body::get_item_thumbnail_size(BodyItem *item) const { sf::Vector2i content_size; sf::Vector2i thumbnail_max_size_scaled = (card_view && card_view_enabled) ? card_max_image_size : sf::Vector2i(thumbnail_max_size.x * get_ui_scale(), 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 = sf::Vector2i(250 * get_ui_scale(), 141 * get_ui_scale()); return content_size; } // TODO: Cache, and take into consideration updated items and visibility change int Body::get_previous_visible_item(int start_index) { for(int i = start_index - 1; i >= 0; --i) { if(items[i]->visible) return i; } return -1; } // TODO: Cache, and take into consideration updated items and visibility change int Body::get_next_visible_item(int start_index) { for(int i = start_index + 1; i < (int)items.size(); ++i) { if(items[i]->visible) return i; } return -1; } void Body::draw_item(sf::RenderWindow &window, std::shared_ptr &item, sf::Vector2f pos, sf::Vector2f size, bool include_embedded_item, bool is_embedded) { // TODO: What about when |card_view| is used? item->keep_alive_frames = 3; get_item_height(item.get(), size.x, true, false, false, -1); draw_item(window, item, pos, size, size.y + body_spacing[body_theme].spacing_y, -1, Json::Value::nullSingleton(), include_embedded_item); } // 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::handle_item_render(const sf::Vector2f pos, const float item_width, const float item_height, int item_index) { if(body_item_select_callback && mouse_left_clicked && !clicked_body_item && click_counts && std::abs(mouse_scroll_accel.y) < 5.0f) { sf::FloatRect item_box(pos + body_pos, sf::Vector2f(item_width, item_height)); if(item_box.contains(mouse_click_pos) && item_box.contains(mouse_release_pos) && mouse_press_pixels_moved_abs <= 25.0) { clicked_body_item = items[item_index]; set_selected_item(item_index, false); } } if(item_index == selected_item) { item_background_target_pos = pos; item_background_target_size = sf::Vector2f(item_width, item_height); item_background_target_height = item_height; if(target_set == TargetSetState::NOT_SET) target_set = TargetSetState::SET; } } float Body::draw_list_view(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size, const int prev_num_visible_items, const Json::Value &content_progress) { const int num_items = items.size(); if(prev_num_visible_items > 0 && render_selected_item_bg && body_theme == BODY_THEME_MINIMAL) { item_background.set_position(sf::Vector2f(pos.x, item_background_prev_pos.y)); item_background.set_size(item_background_prev_size); item_background.set_color(get_current_theme().selected_color); item_background.set_band(sf::Vector2f(0.0f, 0.0f), sf::Vector2f(0.0f, 0.0f)); item_background.draw(window); } int index = -1; if(attach_side == AttachSide::TOP) { if(page_scroll > 0.0) page_scroll = 0.0; pos.y += page_scroll; index = get_next_visible_item(-1); if(pos.y + selected_scrolled > 0.0f) selected_scrolled = 0.0f; } else { if(page_scroll < 0.0) page_scroll = 0.0; pos.y += size.y; pos.y += page_scroll; index = get_previous_visible_item(num_items); if(pos.y + selected_scrolled < size.y) selected_scrolled = 0.0f; } pos.y += selected_scrolled; const float pos_y_start = pos.y; BodyItem *prev_body_item = nullptr; // TODO: Improve performance. Skip items that are obviously not visible or anywhere near the body. Limit loop to 100-200 items around the selected item while(index != -1) { std::shared_ptr &item = items[index]; assert(item->visible); int prev_index; if(attach_side == AttachSide::BOTTOM) { prev_index = get_previous_visible_item(index); if(prev_index == -1) prev_body_item = nullptr; else prev_body_item = items[prev_index].get(); } const bool merge_with_previous = body_item_merge_handler && body_item_merge_handler(prev_body_item, item.get()); if(attach_side == AttachSide::TOP && merge_with_previous) pos.y -= body_spacing[body_theme].spacing_y; get_item_height(item.get(), size.x, false, true, merge_with_previous, index); float top_y; if(attach_side == AttachSide::TOP) top_y = pos.y; else top_y = pos.y - (item->loaded_height + body_spacing[body_theme].spacing_y); if(top_y < 0.0f) { top_cut_off = true; if(index == selected_item) selected_line_top_visible = false; } if(top_y + item->loaded_height > size.y) { bottom_cut_off = true; if(index == selected_item) selected_line_bottom_visible = false; } const bool is_item_visible_in_body = top_y + item->loaded_height >= 0.0f && top_y <= size.y; if(is_item_visible_in_body || index == selected_item) { get_item_height(item.get(), size.x, true, true, merge_with_previous, index); if(attach_side == AttachSide::BOTTOM) pos.y -= (item->loaded_height + body_spacing[body_theme].spacing_y); //page_scroll += add_height; //const float top_y_clamped = clamp(pos.y, 0.0f, size.y); //const float bottom_y_clamped = std::min(pos.y + item->current_loaded_height, size.y); //float offset_y = 0.0f; //if(pos.y < 0.0f) // offset_y = pos.y; //else if(pos.y > size.y) // offset_y = size.y - pos.y; //sf::View new_view(sf::FloatRect(0.0f, 0.0f, window_size.x, scissor_y + bottom_y_clamped)); //new_view.setViewport(sf::FloatRect(0.0f, (scissor_y + top_y_clamped) / (float)window_size.y, 1.0f, (scissor_y + bottom_y_clamped) / (float)window_size.y)); //window.setView(new_view); draw_item(window, item, pos/*sf::Vector2f(pos.x, offset_y)*/, size, item->loaded_height, index, content_progress, true, merge_with_previous); handle_item_render(pos, size.x, item->loaded_height, index); ++num_visible_items; if(first_visible_item == -1 || index < first_visible_item) first_visible_item = index; if(last_visible_item == -1 || index > last_visible_item) last_visible_item = index; if(attach_side == AttachSide::TOP) pos.y += (item->loaded_height + body_spacing[body_theme].spacing_y); } else { if(attach_side == AttachSide::TOP) pos.y += (item->loaded_height + body_spacing[body_theme].spacing_y); else pos.y -= (item->loaded_height + body_spacing[body_theme].spacing_y); if(item->keep_alive_frames == 0) { clear_body_item_cache(item.get()); // TODO: Make sure the embedded item is not referencing another item in the |items| list if(item->embedded_item) clear_body_item_cache(item->embedded_item.get()); } else { --item->keep_alive_frames; } } if(attach_side == AttachSide::BOTTOM && merge_with_previous) pos.y += body_spacing[body_theme].spacing_y; if(attach_side == AttachSide::TOP) { prev_body_item = item.get(); index = get_next_visible_item(index); } else { index = prev_index; } } return pos.y - pos_y_start; } void Body::draw_card_view(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size, sf::Vector2u window_size, float scissor_y) { num_columns = size.x / card_width; int space_left_column = size.x - (num_columns * card_width); int space_left_column_each = 0; // TODO: Spacing for 1 column if(num_columns > 1) { space_left_column_each = space_left_column / (2 + num_columns - 1); if(space_left_column_each < min_column_spacing) { num_columns = (size.x + min_column_spacing) / (card_width + min_column_spacing); //space_left_column_each = min_column_spacing; space_left_column = size.x - (num_columns * card_width); space_left_column_each = num_columns <= 1 ? 0 : space_left_column / (2 + num_columns - 1); } } if(num_columns <= num_columns_switch_to_list) { num_columns = 1; card_view_enabled = false; draw(window, body_pos, body_size); card_view_enabled = true; return; } const int space_left_row_each = space_left_column_each; if(page_scroll > 0.0) page_scroll = 0.0; int item_index = 0; int drawn_column_index = 0; bool row_has_selected_item = false; int num_visible_rows = 1; int row_max_height = 0; const int num_items = items.size(); sf::Vector2f pos_offset(space_left_column_each, page_scroll); while(item_index < num_items) { std::shared_ptr &item = items[item_index]; get_item_height(item.get(), card_max_image_size.x, false, false, false, item_index); int item_height = item->loaded_height; item_height = std::min(card_height, item_height + ((draw_thumbnails && !item->thumbnail_url.empty()) ? card_image_text_padding : 0) + card_padding_y * 2 + 5); row_max_height = std::max(row_max_height, item_height); if(pos_offset.y + item_height <= -body_spacing[body_theme].body_padding_vertical) { top_cut_off = true; if(item_index == selected_item) selected_line_top_visible = false; } if(pos_offset.y >= size.y) { bottom_cut_off = true; if(item_index == selected_item) selected_line_bottom_visible = false; } if(item_index == selected_item) { selected_column = drawn_column_index; row_has_selected_item = true; } if(item->visible && pos_offset.y + item_height > -body_spacing[body_theme].body_padding_vertical && pos_offset.y < size.y) { if(body_item_render_callback) body_item_render_callback(item); sf::Vector2i thumbnail_size = get_item_thumbnail_size(item.get()); std::shared_ptr item_thumbnail; if(draw_thumbnails && !item->thumbnail_url.empty()) item_thumbnail = AsyncImageLoader::get_instance().get_thumbnail(item->thumbnail_url, item->thumbnail_is_local, thumbnail_size); get_item_height(item.get(), card_max_image_size.x, true, false, false, item_index); item_height = item->loaded_height; item_height = std::min(card_height, item_height + (item_thumbnail ? card_image_text_padding : 0) + card_padding_y * 2 + 5); row_max_height = std::max(row_max_height, item_height); handle_item_render(pos + pos_offset, card_width, item_height, item_index); sf::View new_view(sf::FloatRect(0.0f, 0.0f, window_size.x, size.y)); new_view.setViewport(sf::FloatRect(0.0f, scissor_y / (float)window_size.y, 1.0f, size.y / (float)window_size.y)); window.setView(new_view); item_background.set_position(pos + pos_offset); item_background.set_size(sf::Vector2f(card_width, item_height)); item_background.set_color(get_current_theme().card_item_background_color); item_background.set_band(item_background_prev_pos - (pos + pos_offset), item_background_prev_size); item_background.set_band_color(get_current_theme().selected_color); item_background.draw(window); { float image_height = 0.0f; if(item_thumbnail && 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(thumbnail_size); auto new_image_size = clamp_to_size(image_size_f, content_size); auto image_scale = get_ratio(image_size_f, new_image_size); image.setScale(image_scale); image.setPosition(pos + pos_offset + sf::Vector2f(card_padding_x, card_padding_y) + sf::Vector2f(card_max_image_size.x * 0.5f, 0.0f) - sf::Vector2f(new_image_size.x * 0.5f, 0.0f)); image_height = new_image_size.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 if(rounded_rectangle_mask_shader) { rounded_rectangle_mask_shader->setUniform("radius", 10.0f); rounded_rectangle_mask_shader->setUniform("resolution", new_image_size); window.draw(image, rounded_rectangle_mask_shader); } else { window.draw(image); } } else if(!item->thumbnail_url.empty()) { sf::Vector2f content_size = to_vec2f(thumbnail_size); 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(pos + pos_offset + sf::Vector2f(card_padding_x, card_padding_y) + to_vec2f(card_max_image_size) * 0.5f); loading_icon.setScale(get_ratio(loading_icon_size, new_loading_icon_size)); loading_icon.setRotation(elapsed_time_sec * 400.0); window.draw(loading_icon); image_height = content_size.y; } const float text_padding = item_thumbnail ? card_image_text_padding : 0.0f; sf::Vector2f text_pos = sf::Vector2f(pos.x, scissor_y + body_spacing[body_theme].body_padding_vertical) + pos_offset + sf::Vector2f(card_padding_x, card_padding_y) + sf::Vector2f(0.0f, image_height + text_padding); const float text_height = (item_height - card_padding_y * 2.0f) - image_height - text_padding; const float underflow_text = text_pos.y - scissor_y; const float underflow_height = underflow_text < 0.0f ? std::max(0.0f, text_height + underflow_text) : text_height; sf::View new_view(sf::FloatRect(0.0f, 0.0f, window_size.x, underflow_height)); new_view.setViewport(sf::FloatRect(0.0f, std::max(text_pos.y, scissor_y) / (float)window_size.y, 1.0f, underflow_height / (float)window_size.y)); window.setView(new_view); text_pos.y = std::min(0.0f, underflow_text); float text_offset_y = 0.0f; if(item->author_text) { item->author_text->setPosition(text_pos); item->author_text->draw(window); text_offset_y += item->author_text->getHeight(); } if(item->title_text) { item->title_text->setPosition(text_pos + sf::Vector2f(0.0f, text_offset_y)); item->title_text->draw(window); text_offset_y += item->title_text->getHeight(); } if(item->description_text) { item->description_text->setPosition(text_pos + sf::Vector2f(0.0f, text_offset_y)); item->description_text->draw(window); text_offset_y += item->description_text->getHeight(); } const float gradient_height = 5.0f; if(text_offset_y >= text_height - gradient_height && std::abs(item_height - card_height) < 1) { const sf::Vector2f card_bottom(text_pos.x, text_height); const sf::Color color = item_index == selected_item ? get_current_theme().selected_color : get_current_theme().card_item_background_color; sf::Vertex gradient_points[4]; gradient_points[0] = sf::Vertex(card_bottom + sf::Vector2f(0.0f, -gradient_height), sf::Color(color.r, color.g, color.b, 0)); gradient_points[1] = sf::Vertex(card_bottom + sf::Vector2f(card_max_image_size.x, -gradient_height), sf::Color(color.r, color.g, color.b, 0)); gradient_points[2] = sf::Vertex(card_bottom + sf::Vector2f(card_max_image_size.x, 0.0f), color); gradient_points[3] = sf::Vertex(card_bottom + sf::Vector2f(0.0f, 0.0f), color); window.draw(gradient_points, 4, sf::Quads); } } ++num_visible_items; if(first_visible_item == -1 || item_index < first_visible_item) first_visible_item = item_index; if(last_visible_item == -1 || item_index > last_visible_item) last_visible_item = item_index; } else { if(item_index == selected_item) handle_item_render(pos + pos_offset, card_width, item_height, item_index); if(item->keep_alive_frames == 0) { clear_body_item_cache(item.get()); // TODO: Make sure the embedded item is not referencing another item in the |items| list if(item->embedded_item) clear_body_item_cache(item->embedded_item.get()); } else { --item->keep_alive_frames; } } if(item->visible) { pos_offset.x += card_width + space_left_column_each; ++drawn_column_index; if(drawn_column_index == num_columns) { if(row_has_selected_item) item_background_target_size = sf::Vector2f(card_width, row_max_height); drawn_column_index = 0; ++num_visible_rows; pos_offset.x = space_left_column_each; pos_offset.y += row_max_height + space_left_row_each; row_max_height = 0; row_has_selected_item = false; } } ++item_index; } if(row_has_selected_item) item_background_target_size = sf::Vector2f(card_width, row_max_height); } void Body::draw_item(sf::RenderWindow &window, std::shared_ptr &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) { sf::Vector2i thumbnail_size = get_item_thumbnail_size(item.get()); std::shared_ptr item_thumbnail; if(draw_thumbnails && !merge_with_previous && !item->thumbnail_url.empty()) item_thumbnail = AsyncImageLoader::get_instance().get_thumbnail(item->thumbnail_url, item->thumbnail_is_local, thumbnail_size); thumbnail_size = clamp_to_size_x(thumbnail_size, sf::Vector2i(size.x - body_spacing[body_theme].image_padding_x * 2.0f, thumbnail_size.y)); 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(item_index != -1 && body_theme == BODY_THEME_MODERN_SPACIOUS) { item_background.set_size(sf::Vector2f(size.x, item_height)); item_background.set_position(item_pos); item_background.set_color(get_current_theme().shade_color); item_background.set_band(item_background_prev_pos - pos, item_background_prev_size); item_background.set_band_color(get_current_theme().selected_color); item_background.draw(window); } const float padding_y = item_thumbnail ? body_spacing[body_theme].padding_y : body_spacing[body_theme].padding_y_text_only; float text_offset_x = body_spacing[body_theme].padding_x; if(item_thumbnail && !merge_with_previous) { // 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(thumbnail_size); auto new_image_size = clamp_to_size(image_size_f, content_size); auto image_scale = get_ratio(image_size_f, new_image_size); image.setScale(image_scale); image.setPosition(item_pos + sf::Vector2f(body_spacing[body_theme].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 if(rounded_rectangle_mask_shader) { rounded_rectangle_mask_shader->setUniform("radius", 10.0f); rounded_rectangle_mask_shader->setUniform("resolution", new_image_size); window.draw(image, rounded_rectangle_mask_shader); } else { window.draw(image); } text_offset_x += body_spacing[body_theme].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()) { sf::Vector2f content_size = to_vec2f(thumbnail_size); 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(get_current_theme().image_loading_background_color); circle_shape.setPosition(item_pos + sf::Vector2f(body_spacing[body_theme].image_padding_x, padding_y)); window.draw(circle_shape); } else { image_fallback.setSize(content_size); image_fallback.setFillColor(get_current_theme().image_loading_background_color); image_fallback.setPosition(item_pos + sf::Vector2f(body_spacing[body_theme].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(body_spacing[body_theme].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); window.draw(loading_icon); text_offset_x += body_spacing[body_theme].image_padding_x + item->loaded_image_size.x; } } else if(item->thumbnail_size.x > 0) { text_offset_x += body_spacing[body_theme].image_padding_x + item->loaded_image_size.x; } const float text_offset_y = std::floor(6.0f * get_ui_scale()); const float timestamp_text_y = std::floor(item_pos.y + padding_y - text_offset_y); 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 - text_offset_y)); 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 embedded_item_width = std::floor(size.x - text_offset_x - embedded_item_border_width - body_spacing[body_theme].padding_x); float embedded_item_height = item->embedded_item ? get_item_height(item->embedded_item.get(), embedded_item_width, true, false) : ((body_spacing[body_theme].embedded_item_font_size + 5.0f) + body_spacing[body_theme].embedded_item_padding_y * 2.0f); sf::RectangleShape border_left(sf::Vector2f(embedded_item_border_width, std::floor(embedded_item_height))); border_left.setFillColor(get_current_theme().embedded_item_border_color); border_left.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + body_spacing[body_theme].embedded_item_padding_y + 2.0f)); window.draw(border_left); if(item->embedded_item) { sf::Vector2f embedded_item_pos(std::floor(item_pos.x + text_offset_x + embedded_item_border_width + body_spacing[body_theme].padding_x), std::floor(item_pos.y + body_spacing[body_theme].embedded_item_padding_y + 6.0f)); sf::Vector2f embedded_item_size(embedded_item_width, embedded_item_height); draw_item(window, item->embedded_item, 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 + embedded_item_border_width + body_spacing[body_theme].padding_x), std::floor(item_pos.y + embedded_item_height * 0.5f - (body_spacing[body_theme].embedded_item_font_size + 5.0f) * 0.5f + 6.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 - text_offset_y)); 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 - text_offset_y) + height_offset); 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 += body_spacing[body_theme].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->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 + body_spacing[body_theme].reaction_background_padding_x), std::floor(item_pos.y + padding_y - 4.0f + body_spacing[body_theme].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() + body_spacing[body_theme].reaction_background_padding_x * 2.0f, reaction.text->getHeight() + body_spacing[body_theme].reaction_background_padding_y * 2.0f)); reaction_background.draw(window); reaction_offset_x += reaction.text->getWidth() + body_spacing[body_theme].reaction_background_padding_x * 2.0f + body_spacing[body_theme].reaction_spacing_x; reaction.text->draw(window); if(text_offset_x + reaction_offset_x + reaction.text->getWidth() + body_spacing[body_theme].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() + body_spacing[body_theme].reaction_padding_y + text_offset_y; reaction_max_height = reaction.text->getHeight(); } } item_pos.y += reaction_max_height + body_spacing[body_theme].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 - body_spacing[body_theme].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 - body_spacing[body_theme].padding_x), timestamp_text_y + text_offset_y); 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) { const bool rendering_card_view = card_view && card_view_enabled; sf::Vector2i content_size = get_item_thumbnail_size(item); const bool show_thumbnail = draw_thumbnails && !item->thumbnail_url.empty() && !merge_with_previous; if(load_texture && show_thumbnail) { std::shared_ptr item_thumbnail = AsyncImageLoader::get_instance().get_thumbnail(item->thumbnail_url, item->thumbnail_is_local, content_size); content_size = clamp_to_size_x(content_size, sf::Vector2i(width - (rendering_card_view ? 0.0f : body_spacing[body_theme].image_padding_x * 2.0f), content_size.y)); if(item_thumbnail && 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(); 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)); item->loaded_image_size = new_image_size; } else { if(item->loaded_image_size.y < 0.1f) item->loaded_image_size = to_vec2f(content_size); } } else if(item->thumbnail_size.x > 0) { if(!show_thumbnail) item->loaded_image_size.x = content_size.x; // TODO: Fix. This makes the body item have incorrect position when loading and if the item is merge_with_previous? and has an embedded item //if(!merge_with_previous) // image_height = content_size.y; } const float text_offset_x = body_spacing[body_theme].padding_x + body_spacing[body_theme].image_padding_x + item->loaded_image_size.x; const float text_max_width = rendering_card_view ? width : (width - text_offset_x - body_spacing[body_theme].image_padding_x); if(load_texture) update_dirty_state(item, text_max_width); float item_height = 0.0f; bool has_loaded_text = false; if(item->title_text) { item_height += item->title_text->getHeight() - 2.0f + std::floor(3.0f * get_ui_scale()); has_loaded_text = true; } if(item->author_text && !merge_with_previous) { item_height += item->author_text->getHeight() - 2.0f + std::floor(3.0f * get_ui_scale()); has_loaded_text = true; } if(include_embedded_item && item->embedded_item_status != FetchStatus::NONE) { const float embedded_item_width = std::floor(width - text_offset_x - embedded_item_border_width - body_spacing[body_theme].padding_x); if(item->embedded_item) item_height += (get_item_height(item->embedded_item.get(), embedded_item_width, load_texture, false) + 6.0f + body_spacing[body_theme].embedded_item_padding_y * 2.0f); else item_height += ((body_spacing[body_theme].embedded_item_font_size + 5.0f) + 6.0f + body_spacing[body_theme].embedded_item_padding_y * 2.0f); has_loaded_text = true; // TODO: Remove this } if(item->description_text) { item_height += item->description_text->getHeight() - 2.0f; has_loaded_text = true; } if(!item->reactions.empty() && include_embedded_item) { float reaction_offset_x = 0.0f; item_height += body_spacing[body_theme].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(text_max_width); reaction.text->updateGeometry(); reaction_max_height = std::max(reaction_max_height, reaction.text->getHeight()); reaction_offset_x += reaction.text->getWidth() + body_spacing[body_theme].reaction_background_padding_x * 2.0f + body_spacing[body_theme].reaction_spacing_x; if(text_offset_x + reaction_offset_x + reaction.text->getWidth() + body_spacing[body_theme].reaction_background_padding_x * 2.0f > width && i < (int)item->reactions.size() - 1) { reaction_offset_x = 0.0f; item_height += reaction.text->getHeight() + body_spacing[body_theme].reaction_padding_y + std::floor(6.0f * get_ui_scale()); reaction_max_height = reaction.text->getHeight(); } } item_height += reaction_max_height + body_spacing[body_theme].reaction_padding_y; has_loaded_text = true; } // Text can unload. Do not update height if that happens. TODO: Do not unload text, instead clear the internal buffer if(has_loaded_text) item->loaded_content_height = item_height; else item_height = item->loaded_content_height; if(rendering_card_view) { item_height += item->loaded_image_size.y; } else { const bool has_thumbnail = draw_thumbnails && !item->thumbnail_url.empty() && !merge_with_previous; const float padding_y = has_thumbnail ? body_spacing[body_theme].padding_y : body_spacing[body_theme].padding_y_text_only; item_height = std::max(item_height, item->loaded_image_size.y); item_height += (padding_y * 2.0f); } item->loaded_height = item_height; return item_height; } // TODO: Support utf-8 case insensitive find static bool string_find_fuzzy_case_insensitive(const std::string &str, const std::string &substr) { 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) { current_filter = text; if(text.empty()) { for(auto &item : items) { item->visible = true; } return; } for(auto &item : items) { filter_search_fuzzy_item(text, item.get()); } clamp_selection(); using_filter = true; } void Body::filter_search_fuzzy_item(const std::string &text, BodyItem *body_item) { const bool prev_visible = body_item->visible; 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); if(prev_visible && !body_item->visible) { clear_body_item_cache(body_item); // TODO: Make sure the embedded item is not referencing another item in the |items| list if(body_item->embedded_item) clear_body_item_cache(body_item->embedded_item.get()); } } 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) { page_scroll = scroll; } void Body::apply_search_filter_for_item(BodyItem *body_item) { if(current_filter.empty()) return; filter_search_fuzzy_item(current_filter, body_item); } }