diff --git a/server/src/emoji-panel/EmojiPanel.cpp b/server/src/emoji-panel/EmojiPanel.cpp index 494151fa..baa07539 100644 --- a/server/src/emoji-panel/EmojiPanel.cpp +++ b/server/src/emoji-panel/EmojiPanel.cpp @@ -511,6 +511,7 @@ EmojiPanel::EmojiPanel(bool lightTheme) : lightTheme_(lightTheme) { LoadEmojiCatalog(); LoadKaomojiCatalog(); + LoadSymbolCatalog(); searchBox_ = std::make_shared(kSearchHeight * kPanelScale, L"Search"); searchBox_->SetFontSize(14.0f); searchBox_->SetPlaceholderFontSize(12.0f); @@ -677,6 +678,88 @@ void EmojiPanel::LoadKaomojiCatalog() displayDirty_ = true; } +void EmojiPanel::LoadSymbolCatalog() +{ + symbolGroups_.clear(); + symbolTabs_.clear(); + const auto path = OthersDatabasePath(); + if (path.empty() || !std::filesystem::exists(path)) + { + return; + } + + sqlite3 *db = nullptr; + const auto widePath = path.wstring(); + if (sqlite3_open16(widePath.c_str(), &db) != SQLITE_OK) + { + if (db) + { + sqlite3_close(db); + } + return; + } + sqlite3_busy_timeout(db, 3000); + + sqlite3_stmt *stmt = nullptr; + const char *sql = + "SELECT symbol, category, parent_category, keywords FROM symbol_catalog ORDER BY sort_order"; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) + { + sqlite3_close(db); + return; + } + + size_t loaded = 0; + while (sqlite3_step(stmt) == SQLITE_ROW) + { + auto symbol = Utf8ToWide(reinterpret_cast(sqlite3_column_text(stmt, 0))); + auto category = Utf8ToWide(reinterpret_cast(sqlite3_column_text(stmt, 1))); + auto parent = Utf8ToWide(reinterpret_cast(sqlite3_column_text(stmt, 2))); + auto keywords = Utf8ToWide(reinterpret_cast(sqlite3_column_text(stmt, 3))); + if (symbol.empty() || category.empty()) + { + continue; + } + if (parent.empty()) + { + parent = category; + } + if (symbolTabs_.empty() || symbolTabs_.back().title != parent) + { + symbolTabs_.push_back({parent, symbol, {}}); + } + if (symbolGroups_.empty() || symbolGroups_.back().title != category) + { + symbolGroups_.push_back({category, symbol, {}}); + symbolTabs_.back().groupIndices.push_back(symbolGroups_.size() - 1); + if (symbolTabs_.back().icon.empty()) + { + symbolTabs_.back().icon = symbol; + } + } + if (keywords.empty()) + { + keywords = category; + } + const bool longText = symbol.size() > 4; + auto keywordsLower = Lower(keywords); + symbolGroups_.back().items.push_back( + {std::move(symbol), std::move(keywords), std::move(keywordsLower), longText}); + if ((++loaded % 120) == 0 && EmojiPanelSplash::IsVisible()) + { + EmojiPanelSplash::Pump(); + } + } + + sqlite3_finalize(stmt); + sqlite3_close(db); + if (symbolGroups_.empty()) + { + symbolTabs_.clear(); + } + displayDirty_ = true; +} + SizeF EmojiPanel::Measure(const SizeF &availableSize) { return availableSize; @@ -836,6 +919,10 @@ bool EmojiPanel::InDetailPage() const size_t EmojiPanel::EmojiSubTabCount() const { + if (page_ == Page::Symbols) + { + return symbolTabs_.size(); + } return emojiGroups_.size() + 1; } @@ -848,6 +935,15 @@ const EmojiPanel::Group *EmojiPanel::ActiveEmojiGroup() const return &emojiGroups_[emojiSubTab_ - 1]; } +const EmojiPanel::SymbolTab *EmojiPanel::ActiveSymbolTab() const +{ + if (emojiSubTab_ >= symbolTabs_.size()) + { + return nullptr; + } + return &symbolTabs_[emojiSubTab_]; +} + std::vector EmojiPanel::CollectPreviewItems(const std::vector &groups, size_t limit) const { @@ -867,6 +963,40 @@ std::vector EmojiPanel::CollectPreviewItems(const std: return items; } +std::vector EmojiPanel::CollectDiversePreviewItems(const std::vector &groups, + size_t limit) const +{ + std::vector items; + if (limit == 0 || groups.empty()) + { + return items; + } + items.reserve(limit); + size_t round = 0; + while (items.size() < limit) + { + bool added = false; + for (const auto &group : groups) + { + if (round < group.items.size()) + { + items.push_back(&group.items[round]); + added = true; + if (items.size() >= limit) + { + return items; + } + } + } + if (!added) + { + break; + } + ++round; + } + return items; +} + size_t EmojiPanel::HitMainTab(const PointF &point) const { if (InDetailPage()) @@ -885,7 +1015,11 @@ size_t EmojiPanel::HitMainTab(const PointF &point) const size_t EmojiPanel::HitEmojiSubTab(const PointF &point) const { - if (page_ != Page::Emoji) + if (page_ != Page::Emoji && page_ != Page::Symbols) + { + return kInvalidIndex; + } + if (page_ == Page::Symbols && symbolTabs_.empty()) { return kInvalidIndex; } @@ -1022,16 +1156,27 @@ void EmojiPanel::EnsureDisplayLayout() const } appendGroup(L"Kaomoji", std::move(kaomojiPreview), Page::Kaomoji, true); - auto symbolPreview = CollectPreviewItems(symbolGroups_, kPreviewLimit); + auto symbolPreview = CollectDiversePreviewItems(symbolGroups_, kPreviewLimit); if (searching) { symbolPreview.clear(); - if (!symbolGroups_.empty()) + for (const auto &group : symbolGroups_) { - symbolPreview = filterItems(symbolGroups_.front().items); - if (symbolPreview.size() > kPreviewLimit) + for (const auto &item : group.items) { - symbolPreview.resize(kPreviewLimit); + if (item.keywordsLower.find(query) != std::wstring::npos || + item.text.find(searchText_) != std::wstring::npos) + { + symbolPreview.push_back(&item); + if (symbolPreview.size() >= kPreviewLimit) + { + break; + } + } + } + if (symbolPreview.size() >= kPreviewLimit) + { + break; } } } @@ -1065,12 +1210,27 @@ void EmojiPanel::EnsureDisplayLayout() const } else if (page_ == Page::Symbols) { - for (const auto &group : symbolGroups_) + if (searching || symbolTabs_.empty()) { - auto items = filterItems(group.items); - if (!items.empty() || !searching) + for (const auto &group : symbolGroups_) { - appendGroup(group.title, std::move(items), Page::Home, false); + auto items = filterItems(group.items); + if (!items.empty() || !searching) + { + appendGroup(group.title, std::move(items), Page::Home, false); + } + } + } + else if (const SymbolTab *tab = ActiveSymbolTab()) + { + for (const size_t groupIndex : tab->groupIndices) + { + if (groupIndex >= symbolGroups_.size()) + { + continue; + } + const auto &group = symbolGroups_[groupIndex]; + appendGroup(group.title, filterItems(group.items), Page::Home, false); } } } @@ -1605,8 +1765,9 @@ void EmojiPanel::Render(DeviceResources &resources) } DrawChevron(resources, back, text, true); - if (page_ == Page::Emoji) + if (page_ == Page::Emoji || (page_ == Page::Symbols && !symbolTabs_.empty())) { + const bool isSymbolPage = page_ == Page::Symbols; for (size_t index = 0; index < EmojiSubTabCount(); ++index) { const RectF rect = EmojiSubTabRect(index); @@ -1615,7 +1776,14 @@ void EmojiPanel::Render(DeviceResources &resources) FillRect(resources, rect, hover, 6.0f); } std::wstring icon = L"\u23F1"; - if (index > 0 && index - 1 < emojiGroups_.size()) + if (isSymbolPage) + { + if (index < symbolTabs_.size()) + { + icon = symbolTabs_[index].icon; + } + } + else if (index > 0 && index - 1 < emojiGroups_.size()) { icon = emojiGroups_[index - 1].icon; } @@ -1819,6 +1987,8 @@ void EmojiPanel::Render(DeviceResources &resources) emptyText = L"Clipboard history can be connected here"; else if (page_ == Page::Emoji && emojiSubTab_ == 0 && recentItems_.empty()) emptyText = L"Your recently used items will appear here"; + else if (page_ == Page::Symbols && symbolGroups_.empty()) + emptyText = L"Symbol catalog not found"; DrawText(resources, emptyText, {bounds_.x + 30.0f, bounds_.y + kContentTop + 50.0f, bounds_.width - 60.0f, 60.0f}, 20.0f, mutedText, L"Segoe UI", DWRITE_TEXT_ALIGNMENT_CENTER); diff --git a/server/src/emoji-panel/EmojiPanel.h b/server/src/emoji-panel/EmojiPanel.h index 9225b028..67d5fb30 100644 --- a/server/src/emoji-panel/EmojiPanel.h +++ b/server/src/emoji-panel/EmojiPanel.h @@ -59,6 +59,13 @@ class EmojiPanel final : public Visual std::vector items; }; + struct SymbolTab + { + std::wstring title; + std::wstring icon; + std::vector groupIndices; + }; + struct LayoutGroup { std::wstring title; @@ -113,6 +120,7 @@ class EmojiPanel final : public Visual void ResetView(); void LoadEmojiCatalog(); void LoadKaomojiCatalog(); + void LoadSymbolCatalog(); void UpdateSearchPlaceholder(); void ShowToast(std::wstring text); void DismissToast(); @@ -121,24 +129,14 @@ class EmojiPanel final : public Visual bool InDetailPage() const; size_t EmojiSubTabCount() const; const Group *ActiveEmojiGroup() const; + const SymbolTab *ActiveSymbolTab() const; std::vector CollectPreviewItems(const std::vector &groups, size_t limit) const; + std::vector CollectDiversePreviewItems(const std::vector &groups, size_t limit) const; std::vector emojiGroups_; std::vector kaomojiGroups_; - std::vector symbolGroups_ = { - {L"Symbols", L"\u2605", - {{L"★", L"star \u661F\u661F", L"star \u661F\u661F", false}, - {L"☆", L"star outline \u661F\u661F", L"star outline \u661F\u661F", false}, - {L"✓", L"check \u5BF9\u52FE", L"check \u5BF9\u52FE", false}, - {L"✕", L"cross \u9519", L"cross \u9519", false}, - {L"→", L"right arrow \u53F3\u7BAD\u5934", L"right arrow \u53F3\u7BAD\u5934", false}, - {L"←", L"left arrow \u5DE6\u7BAD\u5934", L"left arrow \u5DE6\u7BAD\u5934", false}, - {L"↑", L"up arrow \u4E0A\u7BAD\u5934", L"up arrow \u4E0A\u7BAD\u5934", false}, - {L"↓", L"down arrow", L"down arrow", false}, {L"∞", L"infinity", L"infinity", false}, - {L"©", L"copyright", L"copyright", false}, {L"®", L"registered", L"registered", false}, - {L"™", L"trademark", L"trademark", false}, {L"§", L"section", L"section", false}, - {L"•", L"bullet", L"bullet", false}}}, - }; + std::vector symbolGroups_; + std::vector symbolTabs_; std::vector recentItems_; std::wstring searchText_; std::wstring toastText_;