#include "../include/Body.hpp" #include "../include/Scale.hpp" #include "../include/ResourceLoader.hpp" #include "../include/AsyncImageLoader.hpp" #include "../include/Config.hpp" #include "../include/Utils.hpp" #include "../include/Theme.hpp" #include "../include/StringUtils.hpp" #include #include "../plugins/Plugin.hpp" #include #include #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; }; namespace QuickMedia { static const int card_width = 250.0f * get_config().scale * get_config().font_scale; static const int card_height = 350.0f * get_config().scale * get_config().font_scale; static const int min_column_spacing = 10 * get_config().scale * get_config().spacing_scale; static const int card_padding_x = 20 * get_config().scale * get_config().spacing_scale; static const int card_padding_y = 20 * get_config().scale * get_config().spacing_scale; static const int card_image_text_padding = 10 * get_config().scale * get_config().spacing_scale; static const mgl::vec2i 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; static void init_body_theme_minimal() { body_spacing[BODY_THEME_MINIMAL].spacing_y = std::floor(10.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].padding_x = std::floor(10.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].image_padding_x = std::floor(5.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].padding_y = std::floor(5.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].padding_y_text_only = std::floor(5.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].embedded_item_padding_y = std::floor(0.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].body_padding_horizontal = std::floor(10.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].body_padding_vertical = std::floor(10.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].reaction_background_padding_x = std::floor(7.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].reaction_background_padding_y = std::floor(3.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].reaction_spacing_x = std::floor(5.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].reaction_padding_y = std::floor(7.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].embedded_item_font_size = std::floor(get_config().body.embedded_load_font_size * get_config().scale * get_config().font_scale); } static void init_body_theme_modern_spacious() { body_spacing[BODY_THEME_MODERN_SPACIOUS].spacing_y = std::floor(20.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].padding_x = std::floor(20.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].image_padding_x = std::floor(15.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].padding_y = std::floor(15.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].padding_y_text_only = std::floor(7.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].embedded_item_padding_y = std::floor(0.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].body_padding_horizontal = std::floor(20.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].body_padding_vertical = std::floor(20.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_background_padding_x = std::floor(7.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_background_padding_y = std::floor(3.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_spacing_x = std::floor(5.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_padding_y = std::floor(7.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].embedded_item_font_size = std::floor(get_config().body.embedded_load_font_size * get_config().scale * get_config().font_scale); } void init_body_themes() { if(themes_initialized) return; init_body_theme_minimal(); init_body_theme_modern_spacious(); themes_initialized = true; } Body::Body(BodyTheme body_theme, mgl::Texture &loading_icon_texture, mgl::Shader *rounded_rectangle_shader, mgl::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, get_config().body.progress_font_size * get_config().scale * get_config().font_scale)), replies_text("", *FontLoader::get_font(FontLoader::FontType::LATIN, get_config().body.replies_font_size * get_config().scale * get_config().font_scale)), embedded_item_load_text("", *FontLoader::get_font(FontLoader::FontType::LATIN, body_spacing[body_theme].embedded_item_font_size)), num_visible_items(0), top_cut_off(false), bottom_cut_off(false), item_background(mgl::vec2f(1.0f, 1.0f), 10.0f * get_config().scale, get_theme().selected_color, rounded_rectangle_shader), reaction_background(mgl::vec2f(1.0f, 1.0f), 10.0f * get_config().scale, get_theme().shade_color, rounded_rectangle_shader), rounded_rectangle_shader(rounded_rectangle_shader), rounded_rectangle_mask_shader(rounded_rectangle_mask_shader) { progress_text.set_color(get_theme().text_color); replies_text.set_color(get_theme().replies_text_color); thumbnail_max_size.x = 600; thumbnail_max_size.y = 337; mgl::vec2f loading_icon_size(loading_icon.get_texture()->get_size().x, loading_icon.get_texture()->get_size().y); loading_icon.set_origin(mgl::vec2f(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)); } size_t Body::insert_item_by_timestamp_reverse(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.insert(items.begin(), std::move(body_item)); return 0; } // 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); clamp_selection(); 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 || !items[selected_item]->is_selectable()) 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 || !items[selected_item]->is_selectable()) 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 mgl::Window &window, const mgl::Event &event, bool keyboard_navigation) { if(keyboard_navigation && event.type == mgl::Event::KeyPressed && !event.key.alt) { const bool rendering_card_view = card_view && card_view_enabled; if(event.key.code == mgl::Keyboard::Up || (event.key.control && event.key.code == mgl::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 == mgl::Keyboard::Down || (event.key.control && event.key.code == mgl::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 == mgl::Keyboard::Left) || (event.key.control && event.key.code == mgl::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 == mgl::Keyboard::Right) || (event.key.control && event.key.code == mgl::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 == mgl::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 == mgl::Keyboard::End) { render_selected_item_bg = true; select_last_item(); if(on_bottom_reached) on_bottom_reached(); return true; } else if(event.key.code == mgl::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 == mgl::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 == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left && !mouse_left_pressed && mgl::FloatRect(body_pos, body_size).contains(mgl::vec2f(event.mouse_button.x, event.mouse_button.y))) { mouse_left_pressed = true; mouse_pos_raw.x = event.mouse_button.x; mouse_pos_raw.y = event.mouse_button.y; mouse_pos = mgl::vec2f(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.mouse_button.x < body_pos.x + body_size.x * 0.15f); return true; } else if(event.type == mgl::Event::MouseButtonReleased && event.mouse_button.button == mgl::Mouse::Left && mouse_left_pressed) { mouse_left_pressed = false; mouse_left_clicked = true; mouse_release_pos = mgl::vec2f(event.mouse_button.x, event.mouse_button.y); if(event.mouse_button.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 == mgl::Event::MouseMoved && mouse_left_pressed) { mgl::vec2i mouse_pos_diff(event.mouse_move.x - mouse_pos_raw.x, event.mouse_move.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.mouse_move.x; mouse_pos_raw.y = event.mouse_move.y; render_selected_item_bg = false; return true; } return false; } void Body::draw_drop_shadow(mgl::Window &window) { if(!show_drop_shadow) return; const mgl::Color color(0, 0, 0, 50); const float height = 5.0f; mgl::Vertex gradient_points[4]; gradient_points[0] = mgl::Vertex(body_pos + mgl::vec2f(0.0f, 0.0f), color); gradient_points[1] = mgl::Vertex(body_pos + mgl::vec2f(body_size.x, 0.0f), color); gradient_points[2] = mgl::Vertex(body_pos + mgl::vec2f(body_size.x, height), mgl::Color(color.r, color.g, color.b, 0)); gradient_points[3] = mgl::Vertex(body_pos + mgl::vec2f(0.0f, height), mgl::Color(color.r, color.g, color.b, 0)); window.draw(gradient_points, 4, mgl::PrimitiveType::Quads); } double Body::draw(mgl::Window &window, mgl::vec2f pos, mgl::vec2f size) { return draw(window, pos, size, Json::Value::nullSingleton()); } // TODO: Use a render target for the whole body so all images can be put into one. double Body::draw(mgl::Window &window, mgl::vec2f pos, mgl::vec2f 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; size.x = std::max(0.0f, size.x - 10.0f); if(!rendering_card_view) { pos.x += body_spacing[body_theme].body_padding_horizontal; size.x = std::max(0.0f, size.x - body_spacing[body_theme].body_padding_horizontal * 2.0f); } if(attach_side == AttachSide::TOP) pos.y += body_spacing[body_theme].body_padding_vertical; float frame_time = frame_timer.restart(); if(frame_time > 1.0f) frame_time = 1.0f; 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.get_elapsed_time_seconds(); 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 0.0f; } if(is_touch_enabled()) { const mgl::vec2f mouse_pos_diff(mouse_pos_raw.x - mouse_pos.x, mouse_pos_raw.y - mouse_pos.y); const float move_speed = 35.0f; mgl::vec2f 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 mgl::vec2f mouse_pos_diff_smooth = mouse_pos - prev_mouse_pos; mgl::vec2f 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 = mgl::vec2f(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 // TODO: Selectable item 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 // TODO: Selectable item 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 mgl::vec2i window_size = window.get_size(); const mgl::View prev_view = window.get_view(); if(!rendering_card_view) { mgl::View new_view = { mgl::vec2i(0, scissor_y), mgl::vec2i(window_size.x, size.y) }; window.set_view(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 mgl::vec2f item_background_size_diff = mgl::vec2f(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 mgl::vec2f 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 mgl::vec2f item_background_pos_diff = item_background_target_pos - item_background_prev_pos; const float item_background_move_speed = instant_move ? 1000.0f : speed; mgl::vec2f 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; bool switched_to_list_view = false; double body_total_height = 0.0; if(rendering_card_view) { body_total_height = draw_card_view(window, pos, size, window_size, scissor_y, switched_to_list_view); } else { body_total_height = draw_list_view(window, pos, size, content_progress); } window.set_view(prev_view); if(!switched_to_list_view) draw_drop_shadow(window); const double scrolling_bar_height_ratio = body_total_height == 0.0 ? 0.0 : (size.y / body_total_height); if(!switched_to_list_view && scrolling_bar_height_ratio <= 1.0) { const double scrollbar_max_height = size.y - body_spacing[body_theme].body_padding_vertical * 2.0f; double scrollbar_offset_y = body_total_height == 0.0 ? 0.0 : (std::abs(page_scroll) / body_total_height); RoundedRectangle scrollbar( vec2f_floor(5.0f * get_config().scale, scrollbar_max_height * scrolling_bar_height_ratio), std::floor(3.0f * get_config().scale), get_theme().selected_color, rounded_rectangle_shader); float scrollbar_offset_x = size.x + 10.0f; if(rendering_card_view) scrollbar_offset_x = std::floor(size.x - scrollbar.get_size().x); double scrollbar_y; if(attach_side == AttachSide::TOP) scrollbar_y = std::floor(scissor_y + body_spacing[body_theme].body_padding_vertical + scrollbar_offset_y * scrollbar_max_height); else scrollbar_y = std::floor(scissor_y + body_spacing[body_theme].body_padding_vertical + scrollbar_max_height - scrollbar.get_size().y - scrollbar_offset_y * scrollbar_max_height); scrollbar.set_position( mgl::vec2f(pos.x + scrollbar_offset_x, scrollbar_y)); scrollbar.draw(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 { 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 body_total_height; 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; } return body_total_height; } 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; if(body_item->title_text) { body_item->title_text->setString(body_item->get_title()); body_item->title_text->setMaxWidth(width); } else { body_item->title_text = std::make_unique(body_item->get_title(), false, std::floor(get_config().body.title_font_size * get_config().scale * get_config().font_scale), width, title_mark_urls); } body_item->title_text->set_color(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; if(body_item->description_text) { body_item->description_text->setString(body_item->get_description()); body_item->description_text->setMaxWidth(width); } else { body_item->description_text = std::make_unique(body_item->get_description(), false, std::floor(get_config().body.description_font_size * get_config().scale * get_config().font_scale), width, true); } body_item->description_text->set_color(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; if(body_item->author_text) { body_item->author_text->setString(body_item->get_author()); body_item->author_text->setMaxWidth(width); } else { body_item->author_text = std::make_unique(body_item->get_author(), true, std::floor(get_config().body.author_font_size * get_config().scale * get_config().font_scale), width); } body_item->author_text->set_color(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->set_string(time_str); } else { body_item->timestamp_text = std::make_unique(time_str, *FontLoader::get_font(FontLoader::FontType::LATIN, get_config().body.timestamp_font_size * get_config().scale * get_config().font_scale)); } body_item->timestamp_text->set_color(get_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; } mgl::vec2i Body::get_item_thumbnail_size(BodyItem *item) const { mgl::vec2i content_size; mgl::vec2i thumbnail_max_size_scaled = (card_view && card_view_enabled) ? card_max_image_size : mgl::vec2i(thumbnail_max_size.x * get_config().scale, thumbnail_max_size.y * get_config().scale); if(item->thumbnail_size.x > 0 && item->thumbnail_size.y > 0) content_size = clamp_to_size(mgl::vec2i(std::floor(item->thumbnail_size.x * get_config().scale), std::floor(item->thumbnail_size.y * get_config().scale)), thumbnail_max_size_scaled); else content_size = clamp_to_size(mgl::vec2i(250 * get_config().scale, 141 * get_config().scale), thumbnail_max_size_scaled); 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(mgl::Window &window, std::shared_ptr &item, mgl::vec2f pos, mgl::vec2f 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 mgl::vec2f 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) { mgl::FloatRect item_box(pos + body_pos, mgl::vec2f(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 = mgl::vec2f(item_width, item_height); item_background_target_height = item_height; if(target_set == TargetSetState::NOT_SET) target_set = TargetSetState::SET; } } static float clamp(float value, float min, float max) { return std::min(max, std::max(min, value)); } static mgl::vec2f round(mgl::vec2f vec) { return { std::floor(vec.x), std::floor(vec.y) }; } double Body::draw_list_view(mgl::Window &window, mgl::vec2f pos, mgl::vec2f size, const Json::Value &content_progress) { const int num_items = items.size(); const float pos_y_before_scroll = pos.y; 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 = 0; 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 float pos_y_before = pos.y; 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); RenderItem render_item; render_item.body_item = item; render_item.pos = pos; render_item.size = size; render_item.item_height = item->loaded_height; render_item.item_index = index; render_item.merge_with_previous = merge_with_previous; render_items.push_back(std::move(render_item)); 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; const float pos_y_after = pos.y; item->height = std::abs(pos_y_after - pos_y_before); const float height_diff = item->height - item->prev_height; item->prev_height = item->height; if(attach_side == AttachSide::TOP) { if(index < selected_item) { page_scroll -= height_diff; pos.y -= height_diff; } prev_body_item = item.get(); index = get_next_visible_item(index); } else { if(index > selected_item) { page_scroll += height_diff; pos.y += height_diff; } index = prev_index; } } for(RenderItem &render_item : render_items) { if(render_item.item_index == selected_item) { float offset_y_spacing = -body_spacing[body_theme].spacing_y; if(attach_side == AttachSide::BOTTOM) offset_y_spacing = body_spacing[body_theme].spacing_y; if(selected_item_fits_in_body) { item_background.set_position(round( mgl::vec2f( item_background_prev_pos.x, clamp(item_background_prev_pos.y, pos_y_before_scroll, pos_y_before_scroll + size.y - item_background_prev_size.y - body_spacing[body_theme].body_padding_vertical + offset_y_spacing)))); } else { item_background.set_position(round(item_background_prev_pos)); } item_background.set_size(item_background_prev_size); item_background.set_color(get_theme().selected_color); item_background.draw(window); break; } } for(RenderItem &render_item : render_items) { draw_item(window, render_item.body_item, render_item.pos, render_item.size, render_item.item_height, render_item.item_index, content_progress, true, render_item.merge_with_previous); } render_items.clear(); return std::abs(pos.y - pos_y_start); } double Body::draw_card_view(mgl::Window &window, mgl::vec2f pos, mgl::vec2f size, mgl::vec2i window_size, float scissor_y, bool &switched_to_list_view) { switched_to_list_view = false; 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; const float body_total_height = draw(window, body_pos, body_size); card_view_enabled = true; switched_to_list_view = true; return body_total_height; } 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(); const float pos_y_start = page_scroll; mgl::vec2f 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); mgl::vec2i 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); RenderItem render_item; render_item.body_item = item; render_item.pos = pos; render_item.pos_offset = pos_offset; render_item.item_height = item_height; render_item.item_index = item_index; render_item.item_thumbnail = item_thumbnail; render_items.push_back(std::move(render_item)); handle_item_render(pos + pos_offset, card_width, item_height, item_index); ++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 = mgl::vec2f(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; } mgl::View new_view = { mgl::vec2i(0, scissor_y), mgl::vec2i(window_size.x, size.y) }; window.set_view(new_view); for(RenderItem &render_item : render_items) { if(render_item.item_index == selected_item) { item_background.set_position(round( mgl::vec2f( item_background_prev_pos.x, clamp(item_background_prev_pos.y, pos.y, pos.y + size.y - item_background_prev_size.y - body_spacing[body_theme].body_padding_vertical - body_spacing[body_theme].spacing_y)))); item_background.set_size(round(item_background_prev_size)); item_background.set_color(get_theme().selected_color); item_background.draw(window); break; } } for(RenderItem &render_item : render_items) { draw_card_item(window, render_item.body_item, render_item.pos, render_item.pos_offset, size, mgl::vec2f(window_size.x, window_size.y), render_item.item_height, scissor_y, render_item.item_index, render_item.item_thumbnail.get()); } render_items.clear(); if(row_has_selected_item) item_background_target_size = mgl::vec2f(card_width, row_max_height); return std::abs(pos_offset.y - pos_y_start); } void Body::draw_item(mgl::Window &window, std::shared_ptr &item, const mgl::vec2f &pos, const mgl::vec2f &size, const float item_height, const int item_index, const Json::Value &content_progress, bool include_embedded_item, bool merge_with_previous) { mgl::vec2i 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, size.x - body_spacing[body_theme].image_padding_x * 2.0f); if(body_item_render_callback && include_embedded_item) body_item_render_callback(item); mgl::vec2f item_pos; item_pos.x = std::floor(pos.x); item_pos.y = std::floor(pos.y); const float padding_y = item_thumbnail ? body_spacing[body_theme].padding_y : body_spacing[body_theme].padding_y_text_only; bool thumbnail_drawn = false; 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.is_valid()) { image.set_texture(&item_thumbnail->texture); auto image_size = image.get_texture()->get_size(); mgl::vec2f image_size_f(image_size.x, image_size.y); mgl::vec2f content_size = thumbnail_size.to_vec2f(); auto new_image_size = clamp_to_size(image_size_f, content_size); auto image_scale = get_ratio(image_size_f, new_image_size); image.set_scale(image_scale); image.set_position(item_pos + mgl::vec2f(body_spacing[body_theme].image_padding_x, padding_y)); if(thumbnail_mask_shader && thumbnail_mask_shader->is_valid() && item->thumbnail_mask_type == ThumbnailMaskType::CIRCLE) { thumbnail_mask_shader->set_uniform("resolution", new_image_size); window.draw(image, thumbnail_mask_shader); } else if(rounded_rectangle_mask_shader && rounded_rectangle_mask_shader->is_valid()) { rounded_rectangle_mask_shader->set_uniform("radius", 10.0f); rounded_rectangle_mask_shader->set_uniform("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.set_size(mgl::vec2f(width_ratio * image_size.x, height_ratio * image_size.y)); thumbnail_drawn = true; } else if(!item->thumbnail_url.empty()) { mgl::vec2f content_size = thumbnail_size.to_vec2f(); if(thumbnail_mask_shader && thumbnail_mask_shader->is_valid() && 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 // TODO: Fix //sf::CircleShape circle_shape(content_size.x * 0.5f); //circle_shape.set_color(get_theme().image_loading_background_color); //circle_shape.set_position(item_pos + mgl::vec2f(body_spacing[body_theme].image_padding_x, padding_y)); //window.draw(circle_shape); } else { image_fallback.set_size(content_size); image_fallback.set_color(get_theme().image_loading_background_color); image_fallback.set_position(item_pos + mgl::vec2f(body_spacing[body_theme].image_padding_x, padding_y)); window.draw(image_fallback); } mgl::vec2f loading_icon_size(loading_icon.get_texture()->get_size().x, loading_icon.get_texture()->get_size().y); auto new_loading_icon_size = clamp_to_size(loading_icon_size, content_size); loading_icon.set_position(item_pos + mgl::vec2f(body_spacing[body_theme].image_padding_x, padding_y) + (content_size * 0.5f)); loading_icon.set_scale(get_ratio(loading_icon_size, new_loading_icon_size)); loading_icon.set_rotation(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_config().scale * get_config().font_scale); const float timestamp_text_y = std::floor(item_pos.y + padding_y - text_offset_y - std::floor(4.0f * get_config().scale * get_config().font_scale)); if(item->author_text && !merge_with_previous) { item->author_text->set_position(vec2f_floor(item_pos.x + text_offset_x, item_pos.y + padding_y - text_offset_y)); item->author_text->draw(window); mgl::vec2f replies_text_pos = item->author_text->get_position(); replies_text_pos.x += item->author_text->getWidth() + 5.0f; replies_text.set_position(replies_text_pos); std::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.set_string(std::move(replies_text_str)); window.draw(replies_text); item_pos.y += item->author_text->getHeight() - 2.0f + std::floor(3.0f * get_config().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); mgl::Rectangle border_left(mgl::vec2f(embedded_item_border_width, std::floor(embedded_item_height))); border_left.set_color(get_theme().embedded_item_border_color); border_left.set_position(vec2f_floor(item_pos.x + text_offset_x, item_pos.y + body_spacing[body_theme].embedded_item_padding_y + 2.0f)); window.draw(border_left); if(item->embedded_item) { mgl::vec2f 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)); mgl::vec2f 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.set_string(embedded_item_status_to_string(item->embedded_item_status)); embedded_item_load_text.set_position(vec2f_floor(item_pos.x + text_offset_x + embedded_item_border_width + body_spacing[body_theme].padding_x, item_pos.y + embedded_item_height * 0.5f - (body_spacing[body_theme].embedded_item_font_size + 5.0f) * 0.5f)); window.draw(embedded_item_load_text); } item_pos.y += embedded_item_height + 4.0f; } if(item->title_text) { item->title_text->set_position(vec2f_floor(item_pos.x + text_offset_x, 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_config().scale); } if(item->description_text) { float height_offset = 0.0f; item->description_text->set_position(vec2f_floor(item_pos.x + text_offset_x, 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->set_position(vec2f_floor(item_pos.x + text_offset_x + reaction_offset_x + body_spacing[body_theme].reaction_background_padding_x, item_pos.y + padding_y - 4.0f + body_spacing[body_theme].reaction_background_padding_y)); reaction_background.set_position(vec2f_floor(item_pos.x + text_offset_x + reaction_offset_x, item_pos.y + padding_y)); reaction_background.set_size(mgl::vec2f(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->set_position(vec2f_floor(item_pos.x + size.x - item->timestamp_text->get_bounds().size.x - body_spacing[body_theme].padding_x, timestamp_text_y + 8.0f)); window.draw(*item->timestamp_text); } if(item->extra) { Widgets widgets; if(thumbnail_drawn) { ThumbnailWidget thumbnail; thumbnail.position = image.get_position(); thumbnail.size = image.get_texture()->get_size().to_vec2f() * image.get_scale(); widgets.thumbnail = std::move(thumbnail); } item->extra->draw_overlay(window, widgets); } 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.set_string(std::string("Page: ") + std::to_string(current_json.asInt()) + "/" + std::to_string(total_json.asInt())); auto bounds = progress_text.get_bounds(); progress_text.set_position(vec2f_floor(item_pos.x + size.x - bounds.size.x - body_spacing[body_theme].padding_x, timestamp_text_y + text_offset_y)); window.draw(progress_text); } } } void Body::draw_card_item(mgl::Window &window, std::shared_ptr &item, const mgl::vec2f &pos, const mgl::vec2f &pos_offset, const mgl::vec2f &body_size, const mgl::vec2f &window_size, float item_height, float scissor_y, int item_index, ThumbnailData *item_thumbnail) { const mgl::vec2i thumbnail_size = get_item_thumbnail_size(item.get()); mgl::View new_view = { mgl::vec2i(0, scissor_y), mgl::vec2i(window_size.x, body_size.y) }; window.set_view(new_view); bool thumbnail_drawn = false; { float image_height = 0.0f; if(item_thumbnail && item_thumbnail->loading_state == LoadingState::APPLIED_TO_TEXTURE && item_thumbnail->texture.is_valid()) { image.set_texture(&item_thumbnail->texture); auto image_size = image.get_texture()->get_size(); mgl::vec2f image_size_f(image_size.x, image_size.y); mgl::vec2f content_size = thumbnail_size.to_vec2f(); auto new_image_size = clamp_to_size(image_size_f, content_size); auto image_scale = get_ratio(image_size_f, new_image_size); image.set_scale(image_scale); image.set_position(pos + pos_offset + mgl::vec2f(card_padding_x, card_padding_y) + mgl::vec2f(card_max_image_size.x * 0.5f, 0.0f) - mgl::vec2f(new_image_size.x * 0.5f, 0.0f)); image_height = new_image_size.y; if(thumbnail_mask_shader && thumbnail_mask_shader->is_valid() && item->thumbnail_mask_type == ThumbnailMaskType::CIRCLE) { thumbnail_mask_shader->set_uniform("resolution", new_image_size); window.draw(image, thumbnail_mask_shader); } else if(rounded_rectangle_mask_shader && rounded_rectangle_mask_shader->is_valid()) { rounded_rectangle_mask_shader->set_uniform("radius", 10.0f); rounded_rectangle_mask_shader->set_uniform("resolution", new_image_size); window.draw(image, rounded_rectangle_mask_shader); } else { window.draw(image); } thumbnail_drawn = true; } else if(!item->thumbnail_url.empty()) { mgl::vec2f content_size = thumbnail_size.to_vec2f(); mgl::vec2f loading_icon_size(loading_icon.get_texture()->get_size().x, loading_icon.get_texture()->get_size().y); auto new_loading_icon_size = clamp_to_size(loading_icon_size, content_size); loading_icon.set_position(pos + pos_offset + mgl::vec2f(card_padding_x, card_padding_y) + mgl::vec2f(card_max_image_size.x, content_size.y) * 0.5f); loading_icon.set_scale(get_ratio(loading_icon_size, new_loading_icon_size)); loading_icon.set_rotation(elapsed_time_sec * 400.0); window.draw(loading_icon); image_height = content_size.y; } if(item->extra) { Widgets widgets; if(thumbnail_drawn) { ThumbnailWidget thumbnail; thumbnail.position = image.get_position(); thumbnail.size = image.get_texture()->get_size().to_vec2f() * image.get_scale(); widgets.thumbnail = std::move(thumbnail); } item->extra->draw_overlay(window, widgets); } const float text_padding = item_thumbnail ? card_image_text_padding : 0.0f; mgl::vec2f text_pos = mgl::vec2f(pos.x, scissor_y + body_spacing[body_theme].body_padding_vertical) + pos_offset + mgl::vec2f(card_padding_x, card_padding_y) + mgl::vec2f(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; mgl::View new_view = { mgl::vec2i(0, std::max(text_pos.y, scissor_y)), mgl::vec2i(window_size.x, underflow_height) }; window.set_view(new_view); text_pos.y = std::min(0.0f, underflow_text); float text_offset_y = 0.0f; if(item->author_text) { item->author_text->set_position(text_pos); item->author_text->draw(window); text_offset_y += item->author_text->getHeight(); } if(item->title_text) { item->title_text->set_position(text_pos + mgl::vec2f(0.0f, text_offset_y)); item->title_text->draw(window); text_offset_y += item->title_text->getHeight(); } if(item->description_text) { item->description_text->set_position(text_pos + mgl::vec2f(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 mgl::vec2f card_bottom(text_pos.x, text_height); const mgl::Color color = item_index == selected_item ? get_theme().selected_color : get_theme().background_color; mgl::Vertex gradient_points[4]; gradient_points[0] = mgl::Vertex(card_bottom + mgl::vec2f(0.0f, -gradient_height), mgl::Color(color.r, color.g, color.b, 0)); gradient_points[1] = mgl::Vertex(card_bottom + mgl::vec2f(card_max_image_size.x, -gradient_height), mgl::Color(color.r, color.g, color.b, 0)); gradient_points[2] = mgl::Vertex(card_bottom + mgl::vec2f(card_max_image_size.x, 0.0f), color); gradient_points[3] = mgl::Vertex(card_bottom + mgl::vec2f(0.0f, 0.0f), color); window.draw(gradient_points, 4, mgl::PrimitiveType::Quads); } } } 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; mgl::vec2i 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, width - (rendering_card_view ? 0.0f : body_spacing[body_theme].image_padding_x * 2.0f)); if(item_thumbnail && item_thumbnail->loading_state == LoadingState::FINISHED_LOADING && item_thumbnail->image->get_size().x > 0 && item_thumbnail->image->get_size().y > 0) { if(!item_thumbnail->texture.load_from_image(*item_thumbnail->image)) fprintf(stderr, "Warning: failed to load texture from image: %s\n", item->thumbnail_url.c_str()); //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.is_valid()) { auto image_size = item_thumbnail->texture.get_size(); mgl::vec2f image_size_f(image_size.x, image_size.y); auto new_image_size = clamp_to_size(image_size_f, content_size.to_vec2f()); item->loaded_image_size = new_image_size; } else { if(item->loaded_image_size.y < 0.1f) item->loaded_image_size = content_size.to_vec2f(); } } 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_config().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_config().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) + body_spacing[body_theme].embedded_item_padding_y * 2.0f); else item_height += ((body_spacing[body_theme].embedded_item_font_size + 5.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; } // TODO: Only do this if reactions dirty? 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_config().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); if(item_height > 0.0f) item_height += (padding_y * 2.0f); } item->loaded_height = item_height; return item_height; } 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; if(!body_item->is_selectable()) { body_item->visible = true; } else { 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()); } } // TODO: Selectable 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); } }