/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "iv/iv_search_controller.h" #include "base/invoke_queued.h" #include "iv/iv_search_bar.h" #include "iv/markdown/iv_markdown_article.h" #include "logs.h" #include #include #include #include namespace Iv { using Markdown::MarkdownArticleSearchMatch; SearchController::SearchController( not_null barParent, rpl::producer barWidth, SearchHost host, SearchBarMode barMode) : _barParent(barParent) , _host(std::move(host)) , _searchBar(std::make_unique( barParent, std::move(barWidth), barMode)) { _searchBar->raise(); _searchBar->closeRequests() | rpl::on_next([=] { InvokeQueued(_barParent, [=] { hide(); }); }, _searchBar->lifetime()); _searchBar->queryChanges() | rpl::on_next([=](const QString &query) { applySearchQuery(query); }, _searchBar->lifetime()); _searchBar->navigateRequests() | rpl::on_next([=](int delta) { stepSearchResult(delta); }, _searchBar->lifetime()); _searchBar->focusChanges() | rpl::on_next([=](bool focused) { if (!focused || !_searchBar->shown()) { return; } if (_host.fieldFocused) { _host.fieldFocused(); } refresh(); }, _searchBar->lifetime()); } SearchController::~SearchController() = default; void SearchController::toggle() { if (_searchBar->shown()) { hide(); return; } _searchBar->show(anim::type::normal); _searchBar->setInnerFocus(); refresh(); } void SearchController::hide() { _searchBar->hide(anim::type::normal); _searchBar->setResults(0, 0); if (_host.ready()) { _host.applyMatches({}, -1); _host.focusContent(); } invalidateSearchSession(); _searchEntries.clear(); _searchCurrentEntry = -1; } bool SearchController::shown() const { return _searchBar->shown(); } void SearchController::refresh() { invalidateSearchSession(); if (_searchBar->shown()) { rebuildSearchResults(_searchCurrentEntry, false); } } void SearchController::moveBar(int x, int y) { _searchBar->move(x, y); } void SearchController::raiseBar() { _searchBar->raise(); } rpl::producer SearchController::barHeightValue() const { return _searchBar->heightValue(); } auto SearchController::ScanSearchEntries( const SearchSources &sources, const QString &query) -> std::vector { auto result = std::vector(); if (query.isEmpty()) { return result; } const auto scan = [&](const QString &text, auto &&push) { auto from = 0; while ((from = int(text.indexOf( query, from, Qt::CaseInsensitive))) >= 0) { push(from, from + int(query.size())); from += int(query.size()); } }; for (auto i = 0; i != int(sources.size()); ++i) { const auto &source = sources[i]; scan(source.text, [&](int from, int to) { result.push_back({ .segment = i, .from = from, .to = to }); }); if (!source.hiddenText.isEmpty()) { scan(source.hiddenText, [&](int, int) { result.push_back({ .hiddenDetailsId = source.detailsAnchorId, }); }); } } return result; } void SearchController::ensureSearchSnapshot() { if (_searchSnapshot || !_host.ready()) { return; } _searchSnapshot = std::make_shared( _host.sources()); } void SearchController::invalidateSearchSession() { _searchSnapshot = nullptr; _searchCache.clear(); ++_searchGeneration; } auto SearchController::rescanSearchEntries() -> std::vector { invalidateSearchSession(); ensureSearchSnapshot(); if (!_searchSnapshot || _searchQuery.isEmpty()) { return {}; } auto result = ScanSearchEntries(*_searchSnapshot, _searchQuery); _searchCache.emplace(_searchQuery, result); return result; } void SearchController::applySearchQuery(const QString &query) { _searchQuery = query; rebuildSearchResults(0, true); } void SearchController::rebuildSearchResults( int preferredCurrent, bool activate) { _searchDesiredCurrent = preferredCurrent; _searchDesiredActivate = activate; if (_searchQuery.isEmpty() || !_host.ready()) { applySearchEntries({}, preferredCurrent, activate); return; } const auto i = _searchCache.find(_searchQuery); if (i != end(_searchCache)) { DEBUG_LOG(("Native Markdown IV: search cache hit: %1" ).arg(_searchQuery)); auto entries = i->second; applySearchEntries(std::move(entries), preferredCurrent, activate); return; } if (_searchScanInFlight) { DEBUG_LOG(("Native Markdown IV: search coalesced: %1" ).arg(_searchQuery)); return; } startSearchScan(); } void SearchController::applySearchEntries( std::vector &&entries, int preferredCurrent, bool activate) { _searchEntries = std::move(entries); const auto total = int(_searchEntries.size()); _searchCurrentEntry = total ? std::clamp(preferredCurrent, 0, total - 1) : -1; applyCurrentSearchEntry(activate); } void SearchController::startSearchScan() { Expects(!_searchScanInFlight); ensureSearchSnapshot(); if (!_searchSnapshot) { return; } _searchScanInFlight = true; DEBUG_LOG(("Native Markdown IV: search request: %1" ).arg(_searchQuery)); const auto weak = base::make_weak(this); crl::async([ weak, query = _searchQuery, generation = _searchGeneration, snapshot = _searchSnapshot ] { if (!weak) { return; } auto entries = ScanSearchEntries(*snapshot, query); crl::on_main([ weak, query, generation, entries = std::move(entries) ]() mutable { if (const auto strong = weak.get()) { strong->finishSearchScan( query, generation, std::move(entries)); } }); }); } void SearchController::finishSearchScan( const QString &query, int generation, std::vector &&entries) { _searchScanInFlight = false; if (generation != _searchGeneration) { DEBUG_LOG(("Native Markdown IV: search response dropped: %1" ).arg(query)); } else { DEBUG_LOG(("Native Markdown IV: search response: %1 (%2 matches)" ).arg(query ).arg(int(entries.size()))); _searchCache[query] = entries; } if (!_searchBar->shown() || _searchQuery.isEmpty()) { return; } if (generation == _searchGeneration && query == _searchQuery) { applySearchEntries( std::move(entries), _searchDesiredCurrent, _searchDesiredActivate); } else { rebuildSearchResults( _searchDesiredCurrent, _searchDesiredActivate); } } void SearchController::resolveCurrentSearchEntry() { if (!_host.ready()) { return; } while (_searchCurrentEntry >= 0 && _searchCurrentEntry < int(_searchEntries.size())) { const auto anchorId = _searchEntries[_searchCurrentEntry].hiddenDetailsId; if (anchorId.isEmpty()) { return; } auto runStart = _searchCurrentEntry; while (runStart > 0 && (_searchEntries[runStart - 1].hiddenDetailsId == anchorId)) { --runStart; } auto runEnd = _searchCurrentEntry + 1; while (runEnd < int(_searchEntries.size()) && _searchEntries[runEnd].hiddenDetailsId == anchorId) { ++runEnd; } const auto offset = _searchCurrentEntry - runStart; const auto oldTotal = int(_searchEntries.size()); if (!_host.expandDetails(anchorId)) { return; } _searchEntries = rescanSearchEntries(); const auto newTotal = int(_searchEntries.size()); const auto materialized = newTotal - (oldTotal - (runEnd - runStart)); if (!newTotal) { _searchCurrentEntry = -1; } else if (materialized <= 0) { _searchCurrentEntry = std::min(runStart, newTotal - 1); } else { _searchCurrentEntry = runStart + std::min(offset, materialized - 1); } } } void SearchController::applyCurrentSearchEntry(bool activate) { if (activate) { resolveCurrentSearchEntry(); } const auto total = int(_searchEntries.size()); auto matches = std::vector(); auto currentMatch = -1; auto currentSegment = -1; for (auto i = 0; i != total; ++i) { const auto &entry = _searchEntries[i]; if (entry.segment < 0) { continue; } else if (i == _searchCurrentEntry) { currentMatch = int(matches.size()); currentSegment = entry.segment; } matches.push_back({ .segment = entry.segment, .from = entry.from, .to = entry.to, }); } if (_host.ready()) { _host.applyMatches(std::move(matches), currentMatch); if (activate && currentSegment >= 0) { _host.scrollToSegment(currentSegment); } } _searchBar->setResults(total ? (_searchCurrentEntry + 1) : 0, total); } void SearchController::stepSearchResult(int delta) { const auto total = int(_searchEntries.size()); if (!total || !_host.ready()) { return; } _searchCurrentEntry = (_searchCurrentEntry + delta + total) % total; applyCurrentSearchEntry(true); } } // namespace Iv