#include "../include/Body.hpp" #include "../include/QuickMedia.hpp" #include "../include/Scale.hpp" #include "../include/ResourceLoader.hpp" #include "../include/AsyncImageLoader.hpp" #include "../include/Utils.hpp" #include "../include/StringUtils.hpp" #include "../plugins/Plugin.hpp" #include #include #include #include #include static const sf::Color front_color(32, 36, 42); static const sf::Color back_color(33, 35, 37); static const float spacing_y = std::floor(10.0f * QuickMedia::get_ui_scale()); static const float padding_x = std::floor(10.0f * QuickMedia::get_ui_scale()); static const float image_padding_x = std::floor(5.0f * QuickMedia::get_ui_scale()); static const float padding_y = std::floor(5.0f * QuickMedia::get_ui_scale()); static const float embedded_item_padding_y = std::floor(0.0f * QuickMedia::get_ui_scale()); static const float reaction_background_padding_x = std::floor(7.0f * QuickMedia::get_ui_scale()); static const float reaction_background_padding_y = std::floor(3.0f * QuickMedia::get_ui_scale()); static const float reaction_spacing_x = std::floor(5.0f * QuickMedia::get_ui_scale()); static const float reaction_padding_y = std::floor(7.0f * QuickMedia::get_ui_scale()); static const int embedded_item_font_size = std::floor(14 * QuickMedia::get_ui_scale()); namespace QuickMedia { static float clamp(float value, float min, float max) { return std::min(max, std::max(min, value)); } BodyItem::BodyItem(std::string _title) : visible(true), dirty(false), dirty_description(false), dirty_author(false), dirty_timestamp(false), thumbnail_is_local(false), userdata(nullptr), timestamp(0), title_color(sf::Color::White), author_color(sf::Color::White), description_color(sf::Color::White) { if(!_title.empty()) set_title(std::move(_title)); } BodyItem& BodyItem::operator=(BodyItem &other) { url = other.url; thumbnail_url = other.thumbnail_url; visible = other.visible; dirty = !other.title.empty(); dirty_description = !other.description.empty(); dirty_author = !other.author.empty(); dirty_timestamp = other.timestamp != 0; thumbnail_is_local = other.thumbnail_is_local; title_text.reset(); description_text.reset(); author_text.reset(); timestamp_text.reset(); replies_to = other.replies_to; replies = other.replies; post_number = other.post_number; userdata = other.userdata; prev_last_loaded_height = other.prev_last_loaded_height; last_loaded_height = other.last_loaded_height; current_loaded_height = other.current_loaded_height; embedded_item_status = other.embedded_item_status; if(other.embedded_item) { embedded_item.reset(new BodyItem("")); *embedded_item = *other.embedded_item; } else { embedded_item.reset(); } thumbnail_mask_type = other.thumbnail_mask_type; thumbnail_size = other.thumbnail_size; reactions.clear(); for(auto &reaction : other.reactions) { Reaction reaction_copy; reaction_copy.text = std::make_unique(*reaction.text); reaction_copy.userdata = reaction.userdata; reactions.push_back(std::move(reaction_copy)); } title = other.title; description = other.description; author = other.author; timestamp = other.timestamp; title_color = other.title_color; author_color = other.author_color; description_color = other.description_color; extra = other.extra; keep_alive_frames = other.keep_alive_frames; return *this; } void BodyItem::add_reaction(std::string text, void *userdata) { sf::String str = sf::String::fromUtf8(text.begin(), text.end()); Reaction reaction; reaction.text = std::make_unique(std::move(str), false, std::floor(14 * get_ui_scale()), 0.0f); reaction.userdata = userdata; reactions.push_back(std::move(reaction)); } Body::Body(Program *program, sf::Texture &loading_icon_texture, sf::Shader *rounded_rectangle_shader) : progress_text("", *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(14 * get_ui_scale())), replies_text("", *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(14 * get_ui_scale())), embedded_item_load_text("", *FontLoader::get_font(FontLoader::FontType::LATIN), embedded_item_font_size), draw_thumbnails(true), line_separator_color(sf::Color(32, 37, 43, 255)), body_item_render_callback(nullptr), thumbnail_mask_shader(nullptr), program(program), selected_item(0), prev_selected_item(0), loading_icon(loading_icon_texture), num_visible_items(0), top_cut_off(false), bottom_cut_off(false), item_background(sf::Vector2f(1.0f, 1.0f), 10.0f, sf::Color(55, 60, 68), rounded_rectangle_shader), reaction_background(sf::Vector2f(1.0f, 1.0f), 10.0f, sf::Color(33, 37, 44), rounded_rectangle_shader) { assert(rounded_rectangle_shader); progress_text.setFillColor(sf::Color::White); replies_text.setFillColor(sf::Color(129, 162, 190)); thumbnail_max_size.x = 250; thumbnail_max_size.y = 141; sf::Vector2f loading_icon_size(loading_icon.getTexture()->getSize().x, loading_icon.getTexture()->getSize().y); loading_icon.setOrigin(loading_icon_size.x * 0.5f, loading_icon_size.y * 0.5f); 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); for(int i = 0; i < num_visible_items - 1; ++i) { if(!select_previous_item(false)) return false; } return true; } // TODO: Make this work with wraparound enabled? // TODO: For plugins with different sized body items this can be weird, because after scrolling down thumbnails could load and they could move items up/down until we see items we haven't seen bool Body::select_next_page() { if(!selected_item_fits_in_body) return select_next_item(false); for(int i = 0; i < num_visible_items - 1; ++i) { if(!select_next_item(false)) return false; } return true; } bool Body::select_previous_item(bool scroll_page_if_large_item) { if(items.empty()) return false; if(scroll_page_if_large_item && !selected_item_fits_in_body && !selected_line_top_visible) { selected_scrolled += 128.0f; return true; } const int new_selected_item = get_previous_visible_item(selected_item); if(new_selected_item == -1) return false; selected_item = new_selected_item; return true; } bool Body::select_next_item(bool scroll_page_if_large_item) { if(items.empty()) return false; if(scroll_page_if_large_item && !selected_item_fits_in_body && !selected_line_bottom_visible) { selected_scrolled -= 128.0f; return true; } const int new_selected_item = get_next_visible_item(selected_item); if(new_selected_item == -1) return false; selected_item = new_selected_item; return true; } void Body::set_selected_item(int item, bool reset_prev_selected_item) { //assert(item >= 0 && item < (int)items.size()); selected_item = item; clamp_selection(); //if(reset_prev_selected_item) // prev_selected_item = selected_item; //page_scroll = 0.0f; } void Body::reset_prev_selected_item() { prev_selected_item = selected_item; } int Body::get_index_by_body_item(BodyItem *body_item) { for(int i = 0; i < (int)items.size(); ++i) { if(items[i].get() == body_item) return i; } return -1; } void Body::select_first_item() { selected_item = 0; //if(attach_side == AttachSide::TOP) { // 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::clear_items() { items.clear(); selected_item = 0; prev_selected_item = selected_item; page_scroll = 0.0f; } void Body::prepend_items_reverse(BodyItems new_items) { items.insert(items.begin(), std::make_move_iterator(new_items.rbegin()), std::make_move_iterator(new_items.rend())); items_set_dirty(); } void Body::append_items(BodyItems new_items) { items.insert(items.end(), std::make_move_iterator(new_items.begin()), std::make_move_iterator(new_items.end())); items_set_dirty(); } // 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) { 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) { int new_selected_item = selected_item; for(auto &new_item : new_items) { insert_item_by_timestamp(new_item); if(attach_side == AttachSide::TOP) { new_selected_item = get_previous_visible_item(new_selected_item); } else { new_selected_item = get_next_visible_item(new_selected_item); } } selected_item = new_selected_item; clamp_selection(); items_set_dirty(); prev_selected_item = selected_item; } 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) { 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(event.key.code == sf::Keyboard::Home) { render_selected_item_bg = true; select_first_item(); 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; return true; } else if(event.type == sf::Event::MouseButtonReleased && event.mouseButton.button == sf::Mouse::Left && mouse_left_pressed) { mouse_left_pressed = false; mouse_left_clicked = true; mouse_release_pos = sf::Vector2f(event.mouseButton.x, event.mouseButton.y); return true; } else if(event.type == sf::Event::MouseMoved && mouse_left_pressed) { sf::Vector2i mouse_pos_diff(event.mouseMove.x - mouse_pos_raw.x, event.mouseMove.y - mouse_pos_raw.y); mouse_press_pixels_moved_abs += std::sqrt(mouse_pos_diff.x*mouse_pos_diff.x + mouse_pos_diff.y*mouse_pos_diff.y); mouse_pos_raw.x = event.mouseMove.x; mouse_pos_raw.y = event.mouseMove.y; render_selected_item_bg = false; return true; } return false; } void Body::draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size) { draw(window, pos, size, Json::Value::nullSingleton()); } // TODO: Use a render target for the whole body so all images can be put into one. // TODO: Load thumbnails with more than one thread. void Body::draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size, const Json::Value &content_progress) { if(items_dirty != DirtyState::FALSE) { if(using_filter || items_dirty == DirtyState::FORCE_TRUE) filter_search_fuzzy(current_filter); items_dirty = DirtyState::FALSE; } body_pos = pos; const float scissor_y = pos.y; pos.y = 0.0f; float frame_time = frame_timer.restart().asSeconds(); 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; } bool 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; elapsed_time_sec = draw_timer.getElapsedTime().asSeconds(); const int prev_num_visible_items = num_visible_items; 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; 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; return; } 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]->last_loaded_height < body_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(); 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_y_set == TargetSetState::SET) { target_y_set = TargetSetState::APPLIED; instant_move = true; } const float speed = 30.0f; const float item_background_prev_height = item_background.get_size().y; const float item_background_height_diff = item_background_target_height - item_background_prev_height; const float item_background_height_speed = instant_move ? 1000.0f : speed; const float item_background_new_height = item_background_prev_height + (item_background_height_diff * std::min(1.0f, frame_time * item_background_height_speed)); item_background.set_size(sf::Vector2f(size.x, item_background_new_height)); const float item_background_prev_pos_y = item_background.get_position().y; const float item_background_pos_diff = item_background_target_pos_y - item_background_prev_pos_y; const float item_background_move_speed = instant_move ? 1000.0f : speed; float item_background_new_pos_y = item_background_prev_pos_y + (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, body_size.y - item_background_new_height - spacing_y); item_background_new_pos_y = std::max(item_background_new_pos_y, 0.0f); } item_background.set_position(sf::Vector2f(pos.x, item_background_new_pos_y)); if(prev_num_visible_items > 0 && render_selected_item_bg) item_background.draw(window); int index; 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 pos.y += selected_scrolled; } else { if(page_scroll < 0.0) page_scroll = 0.0; pos.y += body_size.y; pos.y += page_scroll; index = get_previous_visible_item(num_items); if(pos.y + selected_scrolled < body_size.y) selected_scrolled = 0.0f; else pos.y += selected_scrolled; } pos.y += selected_scrolled; BodyItem *prev_body_item = nullptr; const double height_move_speed = 1000.0f; // 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) { BodyItem *item = items[index].get(); 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); if(attach_side == AttachSide::TOP && merge_with_previous) pos.y -= spacing_y; get_item_height(item, size.x, false, true, merge_with_previous, index); const float item_height_diff = item->last_loaded_height - item->current_loaded_height; const float add_height = item_height_diff * std::min(1.0, frame_time * height_move_speed); item->current_loaded_height += add_height; //page_scroll += add_height; float top_y; if(attach_side == AttachSide::TOP) top_y = pos.y; else top_y = pos.y - (item->current_loaded_height + spacing_y); if(top_y < 0.0f) { top_cut_off = true; if(index == selected_item) selected_line_top_visible = false; } if(top_y + item->current_loaded_height > body_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->current_loaded_height >= 0.0f && top_y <= body_size.y; if(is_item_visible_in_body || index == selected_item) { get_item_height(item, size.x, true, true, merge_with_previous, index); const float item_height_diff = item->last_loaded_height - item->current_loaded_height; const float add_height = item_height_diff * std::min(1.0, frame_time * height_move_speed); item->current_loaded_height += add_height; if(attach_side == AttachSide::BOTTOM) pos.y -= (item->current_loaded_height + spacing_y); //page_scroll += add_height; //const float top_y_clamped = clamp(pos.y, 0.0f, body_size.y); //const float bottom_y_clamped = std::min(pos.y + item->current_loaded_height, body_size.y); //float offset_y = 0.0f; //if(pos.y < 0.0f) // offset_y = pos.y; //else if(pos.y > body_size.y) // offset_y = body_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->current_loaded_height, index, content_progress, true, merge_with_previous); handle_item_render(item, pos, size, item->current_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->current_loaded_height + spacing_y); } else { if(attach_side == AttachSide::TOP) pos.y += (item->current_loaded_height + spacing_y); else pos.y -= (item->current_loaded_height + spacing_y); if(item->keep_alive_frames == 0) { clear_body_item_cache(item); // 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 += spacing_y; if(attach_side == AttachSide::TOP) { prev_body_item = item; index = get_next_visible_item(index); } else { index = prev_index; } } window.setView(prev_view); const float item_target_top_diff = item_background_target_pos_y; const float item_target_bottom_diff = (item_background_target_pos_y + item_background_target_height + spacing_y) - body_size.y; if(((body_size_changed && attach_side == AttachSide::BOTTOM) || selected_item_diff < 0) && item_target_top_diff < 0.0f) { //extra_scroll_target -= item_target_top_diff; stuck_direction = StuckDirection::TOP; } else if(((body_size_changed && attach_side == AttachSide::TOP) || selected_item_diff > 0) && 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; } mouse_left_clicked = false; if(clicked_body_item) { auto clicked_body_item_tmp = clicked_body_item; // tmp because below call to body_item_select_callback may call this same draw function clicked_body_item = nullptr; if(body_item_select_callback) body_item_select_callback(clicked_body_item_tmp.get()); } } void Body::update_dirty_state(BodyItem *body_item, float width) { if(body_item->dirty) { body_item->dirty = false; // TODO: Find a way to optimize fromUtf8 sf::String str = sf::String::fromUtf8(body_item->get_title().begin(), body_item->get_title().end()); if(body_item->title_text) body_item->title_text->setString(std::move(str)); else body_item->title_text = std::make_unique(std::move(str), false, std::floor(16 * get_ui_scale()), width, title_mark_urls); body_item->title_text->setFillColor(body_item->get_title_color()); body_item->title_text->updateGeometry(); } if(body_item->dirty_description) { body_item->dirty_description = false; sf::String str = sf::String::fromUtf8(body_item->get_description().begin(), body_item->get_description().end()); if(body_item->description_text) body_item->description_text->setString(std::move(str)); else body_item->description_text = std::make_unique(std::move(str), false, std::floor(14 * get_ui_scale()), width, true); body_item->description_text->setFillColor(body_item->get_description_color()); body_item->description_text->updateGeometry(); } if(body_item->dirty_author) { body_item->dirty_author = false; sf::String str = sf::String::fromUtf8(body_item->get_author().begin(), body_item->get_author().end()); if(body_item->author_text) body_item->author_text->setString(std::move(str)); else body_item->author_text = std::make_unique(std::move(str), true, std::floor(14 * get_ui_scale()), width); body_item->author_text->setFillColor(body_item->get_author_color()); body_item->author_text->updateGeometry(); } if(body_item->dirty_timestamp) { body_item->dirty_timestamp = false; 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}; /* if(is_same_year) strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M:%S", message_tm); else strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M:%S %Y", message_tm); */ strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M", &message_tm); if(body_item->timestamp_text) body_item->timestamp_text->setString(time_str); else body_item->timestamp_text = std::make_unique(time_str, *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(10 * get_ui_scale())); body_item->timestamp_text->setFillColor(sf::Color(185, 190, 198, 100)); } } } void Body::clear_body_item_cache(BodyItem *body_item) { if(body_item->title_text) { body_item->title_text.reset(); body_item->dirty = true; } if(body_item->description_text) { body_item->description_text.reset(); body_item->dirty_description = true; } if(body_item->author_text) { body_item->author_text.reset(); body_item->dirty_author = true; } if(body_item->timestamp_text) { body_item->timestamp_text.reset(); body_item->dirty_timestamp = true; } 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(std::floor(thumbnail_max_size.x * get_ui_scale()), std::floor(thumbnail_max_size.y * get_ui_scale())); if(item->thumbnail_size.x > 0 && item->thumbnail_size.y > 0) content_size = clamp_to_size(sf::Vector2i(std::floor(item->thumbnail_size.x * get_ui_scale()), std::floor(item->thumbnail_size.y * get_ui_scale())), thumbnail_max_size_scaled); else content_size = thumbnail_max_size_scaled; return content_size; } // TODO: Cache, and take into consideration updated items and visibility change 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; } static sf::Vector2f to_vec2f(const sf::Vector2i &vec) { return sf::Vector2f(vec.x, vec.y); } void Body::draw_item(sf::RenderWindow &window, BodyItem *item, sf::Vector2f pos, sf::Vector2f size, bool include_embedded_item, bool is_embedded) { item->keep_alive_frames = 3; get_item_height(item, size.x, true, false, false, -1); draw_item(window, item, pos, size, size.y + 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(BodyItem *item, const sf::Vector2f pos, const sf::Vector2f size, 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(size.x, 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_y = pos.y; item_background_target_height = item_height; if(target_y_set == TargetSetState::NOT_SET) target_y_set = TargetSetState::SET; } } void Body::draw_item(sf::RenderWindow &window, BodyItem *item, const sf::Vector2f &pos, const sf::Vector2f &size, const float item_height, const int item_index, const Json::Value &content_progress, bool include_embedded_item, bool merge_with_previous) { sf::Vector2i thumbnail_size = get_item_thumbnail_size(item); 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, 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); float text_offset_x = 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(image_padding_x, padding_y)); if(thumbnail_mask_shader && item->thumbnail_mask_type == ThumbnailMaskType::CIRCLE) { thumbnail_mask_shader->setUniform("resolution", new_image_size); window.draw(image, thumbnail_mask_shader); } else { window.draw(image); } text_offset_x += image_padding_x + new_image_size.x; // We want the next image fallback to have the same size as the successful image rendering, because its likely the image fallback will have the same size (for example thumbnails on youtube) //image_fallback.setSize(sf::Vector2f(width_ratio * image_size.x, height_ratio * image_size.y)); } else if(!item->thumbnail_url.empty()) { sf::Vector2f content_size = to_vec2f(thumbnail_size); sf::Color fallback_color(52, 58, 70); if(thumbnail_mask_shader && item->thumbnail_mask_type == ThumbnailMaskType::CIRCLE) { // TODO: Use the mask shader instead, but a vertex shader is also needed for that to pass the vertex coordinates since // shapes dont have texture coordinates. // TODO: Cache circle shape sf::CircleShape circle_shape(content_size.x * 0.5f); circle_shape.setFillColor(fallback_color); circle_shape.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y)); window.draw(circle_shape); } else { image_fallback.setSize(content_size); image_fallback.setFillColor(fallback_color); image_fallback.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y)); window.draw(image_fallback); } sf::Vector2f loading_icon_size(loading_icon.getTexture()->getSize().x, loading_icon.getTexture()->getSize().y); auto new_loading_icon_size = clamp_to_size(loading_icon_size, content_size); loading_icon.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y) + (content_size * 0.5f)); loading_icon.setScale(get_ratio(loading_icon_size, new_loading_icon_size)); loading_icon.setRotation(elapsed_time_sec * 400.0); window.draw(loading_icon); text_offset_x += image_padding_x + content_size.x; } } else if(item->thumbnail_size.x > 0) { text_offset_x += image_padding_x + thumbnail_size.x; } const float timestamp_text_y = std::floor(item_pos.y + padding_y - std::floor(6.0f * get_ui_scale())); if(item->author_text && !merge_with_previous) { item->author_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 6.0f * get_ui_scale())); item->author_text->setMaxWidth(size.x - text_offset_x - image_padding_x); item->author_text->draw(window); sf::Vector2f replies_text_pos = item->author_text->getPosition() + sf::Vector2f(0.0f, 5.0f); replies_text_pos.x += item->author_text->getWidth() + 5.0f; replies_text.setPosition(replies_text_pos); sf::String replies_text_str; for(size_t reply_index : item->replies) { BodyItem *reply_item = items[reply_index].get(); replies_text_str += " >>"; replies_text_str += reply_item->post_number; } replies_text.setString(std::move(replies_text_str)); window.draw(replies_text); item_pos.y += item->author_text->getHeight() - 2.0f + std::floor(3.0f * get_ui_scale()); } if(include_embedded_item && item->embedded_item_status != FetchStatus::NONE) { const float border_width = 4.0f; const float embedded_item_width = std::floor(size.x - text_offset_x - border_width - padding_x); float embedded_item_height = item->embedded_item ? get_item_height(item->embedded_item.get(), embedded_item_width, true, false) : ((embedded_item_font_size + 5.0f) + embedded_item_padding_y * 2.0f); sf::RectangleShape border_left(sf::Vector2f(border_width, std::floor(embedded_item_height))); border_left.setFillColor(sf::Color::White); border_left.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + 4.0f)); window.draw(border_left); if(item->embedded_item) { sf::Vector2f embedded_item_pos(std::floor(item_pos.x + text_offset_x + border_width + padding_x), std::floor(item_pos.y + embedded_item_padding_y + 4.0f)); sf::Vector2f embedded_item_size(embedded_item_width, embedded_item_height); draw_item(window, item->embedded_item.get(), embedded_item_pos, embedded_item_size, false, true); } else { embedded_item_load_text.setString(embedded_item_status_to_string(item->embedded_item_status)); embedded_item_load_text.setPosition(std::floor(item_pos.x + text_offset_x + border_width + padding_x), std::floor(item_pos.y + embedded_item_height * 0.5f - (embedded_item_font_size + 5.0f) * 0.5f + 4.0f)); window.draw(embedded_item_load_text); } item_pos.y += embedded_item_height + 4.0f; } //title_text.setString(item->title); //title_text.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y)); //window.draw(title_text); if(item->title_text) { item->title_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - std::floor(6.0f * get_ui_scale()))); item->title_text->setMaxWidth(size.x - text_offset_x - image_padding_x); item->title_text->draw(window); item_pos.y += item->title_text->getHeight() - 2.0f + std::floor(3.0f * get_ui_scale()); } if(item->description_text) { float height_offset = 0.0f; item->description_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - std::floor(6.0f * get_ui_scale()) + height_offset)); item->description_text->setMaxWidth(size.x - text_offset_x - image_padding_x); item->description_text->draw(window); item_pos.y += item->description_text->getHeight() - 2.0f; } if(!item->reactions.empty() && include_embedded_item) { float reaction_offset_x = 0.0f; item_pos.y += reaction_padding_y; float reaction_max_height = 0.0f; // TODO: Fix first row wrap-around for(int i = 0; i < item->reactions.size(); ++i) { auto &reaction = item->reactions[i]; reaction.text->setMaxWidth(size.x - text_offset_x - image_padding_x); reaction.text->updateGeometry(); reaction_max_height = std::max(reaction_max_height, reaction.text->getHeight()); reaction.text->setPosition(std::floor(item_pos.x + text_offset_x + reaction_offset_x + reaction_background_padding_x), std::floor(item_pos.y + padding_y - 4.0f + reaction_background_padding_y)); reaction_background.set_position(sf::Vector2f(std::floor(item_pos.x + text_offset_x + reaction_offset_x), std::floor(item_pos.y + padding_y))); reaction_background.set_size(sf::Vector2f(reaction.text->getWidth() + reaction_background_padding_x * 2.0f, reaction.text->getHeight() + reaction_background_padding_y * 2.0f)); reaction_background.draw(window); reaction_offset_x += reaction.text->getWidth() + reaction_background_padding_x * 2.0f + reaction_spacing_x; reaction.text->draw(window); if(text_offset_x + reaction_offset_x + reaction.text->getWidth() + reaction_background_padding_x * 2.0f > size.x && i < (int)item->reactions.size() - 1) { reaction_offset_x = 0.0f; item_pos.y += reaction.text->getHeight() + reaction_padding_y + std::floor(6.0f * get_ui_scale()); reaction_max_height = reaction.text->getHeight(); } } item_pos.y += reaction_max_height + reaction_padding_y; } if(item_index == selected_item && item->timestamp_text) { item->timestamp_text->setPosition(std::floor(item_pos.x + size.x - item->timestamp_text->getLocalBounds().width - padding_x), timestamp_text_y + 8.0f); window.draw(*item->timestamp_text); } if(!content_progress.isObject()) return; // TODO: Do the same for non-manga content. // TODO: Cache this instead of hash access every item every frame. const Json::Value &item_progress = content_progress[item->get_title()]; if(item_progress.isObject()) { const Json::Value ¤t_json = item_progress["current"]; const Json::Value &total_json = item_progress["total"]; if(current_json.isNumeric() && total_json.isNumeric()) { progress_text.setString(std::string("Page: ") + std::to_string(current_json.asInt()) + "/" + std::to_string(total_json.asInt())); auto bounds = progress_text.getLocalBounds(); progress_text.setPosition(std::floor(item_pos.x + size.x - bounds.width - padding_x), timestamp_text_y + std::floor(6.0f * get_ui_scale())); window.draw(progress_text); } } } float Body::get_item_height(BodyItem *item, float width, bool load_texture, bool include_embedded_item, bool merge_with_previous, int item_index) { sf::Vector2i content_size = get_item_thumbnail_size(item); float image_height = 0.0f; float text_offset_x = padding_x; if(draw_thumbnails && load_texture && !item->thumbnail_url.empty() && !merge_with_previous) { 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, content_size.y)); image_height = 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)); image_height = new_image_size.y; text_offset_x += image_padding_x + new_image_size.x; } else { text_offset_x += image_padding_x + content_size.x; } } else if(item->thumbnail_size.x > 0) { text_offset_x += image_padding_x + content_size.x; if(!merge_with_previous) image_height = content_size.y; } if(load_texture) update_dirty_state(item, width - text_offset_x - image_padding_x); float item_height = 0.0f; if(item->title_text) { item_height += item->title_text->getHeight() - 2.0f + std::floor(3.0f * get_ui_scale()); } if(item->author_text && !merge_with_previous) { item_height += item->author_text->getHeight() - 2.0f + std::floor(3.0f * get_ui_scale()); } if(include_embedded_item && item->embedded_item_status != FetchStatus::NONE) { if(item->embedded_item) item_height += (get_item_height(item->embedded_item.get(), width, load_texture, false) + 4.0f + embedded_item_padding_y * 2.0f); else item_height += ((embedded_item_font_size + 5.0f) + 4.0f + embedded_item_padding_y * 2.0f); } if(item->description_text) { item_height += item->description_text->getHeight() - 2.0f; } if(!item->reactions.empty() && include_embedded_item) { float reaction_offset_x = 0.0f; item_height += reaction_padding_y; float reaction_max_height = 0.0f; for(int i = 0; i < item->reactions.size(); ++i) { auto &reaction = item->reactions[i]; reaction.text->setMaxWidth(width - text_offset_x - image_padding_x); reaction.text->updateGeometry(); reaction_max_height = std::max(reaction_max_height, reaction.text->getHeight()); reaction_offset_x += reaction.text->getWidth() + reaction_background_padding_x * 2.0f + reaction_spacing_x; if(text_offset_x + reaction_offset_x + reaction.text->getWidth() + reaction_background_padding_x * 2.0f > width && i < (int)item->reactions.size() - 1) { reaction_offset_x = 0.0f; item_height += reaction.text->getHeight() + reaction_padding_y + std::floor(6.0f * get_ui_scale()); reaction_max_height = reaction.text->getHeight(); } } item_height += reaction_max_height + reaction_padding_y; } item_height = std::max(item_height, image_height); item_height += (padding_y * 2.0f); const bool first_height_set = item->last_loaded_height < 0.01f; if(item_index != -1 && (first_height_set || load_texture)) { const float height_diff = item_height - item->prev_last_loaded_height; //if(attach_side == AttachSide::TOP) { // if(item_index < selected_item) // extra_scroll_target -= height_diff; //} else if(attach_side == AttachSide::BOTTOM) { // if(item_index > selected_item) // extra_scroll_target += height_diff; //} item->last_loaded_height = item_height; item->prev_last_loaded_height = item_height; if(first_height_set) item->current_loaded_height = item_height; } return item_height; } float Body::get_spacing_y() const { return spacing_y; } // TODO: Support utf-8 case insensitive find static bool string_find_fuzzy_case_insensitive(const std::string &str, const std::string &substr) { if(str.empty()) return false; if(substr.empty()) return true; size_t str_index = 0; bool full_match = true; string_split(substr, ' ', [&str, &str_index, &full_match](const char *str_part, size_t size) { if(size == 0) return true; size_t found_index = str_find_case_insensitive(str, str_index, str_part, size); if(found_index == std::string::npos) { full_match = false; return false; } str_index = found_index + size; return true; }); return full_match; } void Body::filter_search_fuzzy(const std::string &text) { 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) { body_item->visible = string_find_fuzzy_case_insensitive(body_item->get_title(), text); if(!body_item->visible && !body_item->get_description().empty()) body_item->visible = string_find_fuzzy_case_insensitive(body_item->get_description(), text); if(!body_item->visible && !body_item->get_author().empty()) body_item->visible = string_find_fuzzy_case_insensitive(body_item->get_author(), text); } bool Body::no_items_visible() const { for(auto &item : items) { if(item->visible) return false; } return true; } bool Body::is_selected_item_last_visible_item() const { if(selected_item < 0 || selected_item >= (int)items.size() || !items[selected_item]->visible) return false; for(int i = selected_item + 1; i < (int)items.size(); ++i) { if(items[i]->visible) return false; } return true; } void Body::set_page_scroll(float scroll) { page_scroll = scroll; } void Body::items_set_dirty(bool force) { items_dirty = force ? DirtyState::FORCE_TRUE : DirtyState::TRUE; } void Body::apply_search_filter_for_item(BodyItem *body_item) { filter_search_fuzzy_item(current_filter, body_item); } }