/* 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 "info/media/info_media_list_section.h" #include "storage/storage_shared_media.h" #include "layout/layout_selection.h" #include "styles/style_info.h" namespace Info::Media { namespace { constexpr auto kFloatingHeaderAlpha = 0.9; } // namespace ListSection::ListSection(Type type, not_null delegate) : _type(type) , _delegate(delegate) , _hasFloatingHeader(delegate->sectionHasFloatingHeader()) , _mosaic(st::emojiPanWidth - st::inlineResultsLeft) { } bool ListSection::empty() const { return _items.empty(); } UniversalMsgId ListSection::minId() const { Expects(!empty()); return GetUniversalId(_items.back()->getItem()); } void ListSection::setTop(int top) { _top = top; } int ListSection::top() const { return _top; } int ListSection::height() const { return _height; } int ListSection::bottom() const { return top() + height(); } bool ListSection::addItem(not_null item) { if (_items.empty() || belongsHere(item)) { if (_items.empty()) { setHeader(item); } appendItem(item); return true; } return false; } void ListSection::finishSection() { if (_type == Type::GIF) { _mosaic.setOffset(st::infoMediaSkip, headerHeight()); _mosaic.setRightSkip(st::infoMediaSkip); _mosaic.addItems(_items); } } void ListSection::setHeader(not_null item) { _header.setText(st::infoMediaHeaderStyle, _delegate->sectionTitle(item)); } bool ListSection::belongsHere( not_null item) const { Expects(!_items.empty()); return _delegate->sectionItemBelongsHere(item, _items.back()); } void ListSection::appendItem(not_null item) { _items.push_back(item); _byItem.emplace(item->getItem(), item); } bool ListSection::removeItem(not_null item) { if (const auto i = _byItem.find(item); i != end(_byItem)) { _items.erase(ranges::remove(_items, i->second), end(_items)); _byItem.erase(i); refreshHeight(); return true; } return false; } QRect ListSection::findItemRect( not_null item) const { auto position = item->position(); if (!_mosaic.empty()) { return _mosaic.findRect(position); } auto top = position / _itemsInRow; auto indexInRow = position % _itemsInRow; auto left = _itemsLeft + indexInRow * (_itemWidth + st::infoMediaSkip); return QRect(left, top, _itemWidth, item->height()); } ListFoundItem ListSection::completeResult( not_null item, bool exact) const { return { item, findItemRect(item), exact }; } ListFoundItem ListSection::findItemByPoint(QPoint point) const { Expects(!_items.empty()); if (!_mosaic.empty()) { const auto found = _mosaic.findByPoint(point); Assert(found.index != -1); const auto item = _mosaic.itemAt(found.index); const auto rect = findItemRect(item); return { item, rect, found.exact }; } auto itemIt = findItemAfterTop(point.y()); if (itemIt == end(_items)) { --itemIt; } auto item = *itemIt; auto rect = findItemRect(item); if (point.y() >= rect.top()) { auto shift = floorclamp( point.x(), (_itemWidth + st::infoMediaSkip), 0, _itemsInRow); while (shift-- && itemIt != _items.end()) { ++itemIt; } if (itemIt == _items.end()) { --itemIt; } item = *itemIt; rect = findItemRect(item); } return { item, rect, rect.contains(point) }; } std::optional ListSection::findItemByItem( not_null item) const { const auto i = _byItem.find(item); if (i != end(_byItem)) { return ListFoundItem{ i->second, findItemRect(i->second), true }; } return std::nullopt; } ListFoundItem ListSection::findItemDetails( not_null item) const { return { item, findItemRect(item), true }; } auto ListSection::findItemAfterTop( int top) -> Items::iterator { Expects(_mosaic.empty()); return ranges::lower_bound( _items, top, std::less_equal<>(), [this](const auto &item) { const auto itemTop = item->position() / _itemsInRow; return itemTop + item->height(); }); } auto ListSection::findItemAfterTop( int top) const -> Items::const_iterator { Expects(_mosaic.empty()); return ranges::lower_bound( _items, top, std::less_equal<>(), [this](const auto &item) { const auto itemTop = item->position() / _itemsInRow; return itemTop + item->height(); }); } auto ListSection::findItemAfterBottom( Items::const_iterator from, int bottom) const -> Items::const_iterator { Expects(_mosaic.empty()); return ranges::lower_bound( from, _items.end(), bottom, std::less<>(), [this](const auto &item) { const auto itemTop = item->position() / _itemsInRow; return itemTop; }); } const ListSection::Items &ListSection::items() const { return _items; } void ListSection::paint( Painter &p, const ListContext &context, QRect clip, int outerWidth) const { auto header = headerHeight(); if (QRect(0, 0, outerWidth, header).intersects(clip)) { p.setPen(st::infoMediaHeaderFg); _header.drawLeftElided( p, st::infoMediaHeaderPosition.x(), st::infoMediaHeaderPosition.y(), outerWidth - 2 * st::infoMediaHeaderPosition.x(), outerWidth); } auto localContext = context.layoutContext; if (!_mosaic.empty()) { auto paintItem = [&](not_null item, QPoint point) { p.translate(point.x(), point.y()); item->paint( p, clip.translated(-point), itemSelection(item, context), &localContext); p.translate(-point.x(), -point.y()); }; _mosaic.paint(std::move(paintItem), clip); return; } auto fromIt = findItemAfterTop(clip.y()); auto tillIt = findItemAfterBottom( fromIt, clip.y() + clip.height()); for (auto it = fromIt; it != tillIt; ++it) { auto item = *it; auto rect = findItemRect(item); localContext.skipBorder = (rect.y() <= header + _itemsTop); if (rect.intersects(clip)) { p.translate(rect.topLeft()); item->paint( p, clip.translated(-rect.topLeft()), itemSelection(item, context), &localContext); p.translate(-rect.topLeft()); } } } void ListSection::paintFloatingHeader( Painter &p, int visibleTop, int outerWidth) { if (!_hasFloatingHeader) { return; } const auto headerTop = st::infoMediaHeaderPosition.y() / 2; if (visibleTop <= (_top + headerTop)) { return; } const auto header = headerHeight(); const auto headerLeft = st::infoMediaHeaderPosition.x(); const auto floatingTop = std::min( visibleTop, bottom() - header + headerTop); p.save(); p.resetTransform(); p.setOpacity(kFloatingHeaderAlpha); p.fillRect(QRect(0, floatingTop, outerWidth, header), st::boxBg); p.setOpacity(1.0); p.setPen(st::infoMediaHeaderFg); _header.drawLeftElided( p, headerLeft, floatingTop + headerTop, outerWidth - 2 * headerLeft, outerWidth); p.restore(); } TextSelection ListSection::itemSelection( not_null item, const ListContext &context) const { const auto parent = item->getItem(); auto dragSelectAction = context.dragSelectAction; if (dragSelectAction != ListDragSelectAction::None) { auto i = context.dragSelected->find(parent); if (i != context.dragSelected->end()) { return (dragSelectAction == ListDragSelectAction::Selecting) ? FullSelection : TextSelection(); } } auto i = context.selected->find(parent); return (i == context.selected->cend()) ? TextSelection() : i->second.text; } int ListSection::headerHeight() const { return _header.isEmpty() ? 0 : st::infoMediaHeaderHeight; } void ListSection::resizeToWidth(int newWidth) { auto minWidth = st::infoMediaMinGridSize + st::infoMediaSkip * 2; if (newWidth < minWidth) { return; } auto resizeOneColumn = [&](int itemsLeft, int itemWidth) { _itemsLeft = itemsLeft; _itemsTop = 0; _itemsInRow = 1; _itemWidth = itemWidth; for (auto &item : _items) { item->resizeGetHeight(_itemWidth); } }; switch (_type) { case Type::Photo: case Type::Video: case Type::RoundFile: { _itemsLeft = st::infoMediaSkip; _itemsTop = st::infoMediaSkip; _itemsInRow = (newWidth - _itemsLeft) / (st::infoMediaMinGridSize + st::infoMediaSkip); _itemWidth = ((newWidth - _itemsLeft) / _itemsInRow) - st::infoMediaSkip; for (auto &item : _items) { item->resizeGetHeight(_itemWidth); } } break; case Type::GIF: { _mosaic.setFullWidth(newWidth - st::infoMediaSkip); } break; case Type::RoundVoiceFile: case Type::MusicFile: resizeOneColumn(0, newWidth); break; case Type::File: case Type::Link: { auto itemsLeft = st::infoMediaHeaderPosition.x(); auto itemWidth = newWidth - 2 * itemsLeft; resizeOneColumn(itemsLeft, itemWidth); } break; } refreshHeight(); } int ListSection::recountHeight() { auto result = headerHeight(); switch (_type) { case Type::Photo: case Type::Video: case Type::RoundFile: { auto itemHeight = _itemWidth + st::infoMediaSkip; auto index = 0; result += _itemsTop; for (auto &item : _items) { item->setPosition(_itemsInRow * result + index); if (++index == _itemsInRow) { result += itemHeight; index = 0; } } if (_items.size() % _itemsInRow) { _rowsCount = int(_items.size()) / _itemsInRow + 1; result += itemHeight; } else { _rowsCount = int(_items.size()) / _itemsInRow; } } break; case Type::GIF: { return _mosaic.countDesiredHeight(0) + result; } break; case Type::RoundVoiceFile: case Type::File: case Type::MusicFile: case Type::Link: for (auto &item : _items) { item->setPosition(result); result += item->height(); } _rowsCount = _items.size(); break; } return result; } void ListSection::refreshHeight() { _height = recountHeight(); } } // namespace Info::Media