🐱
Siv3D | 絵文字を :cat: のようなエイリアスで入力する
絵文字を :cat: のようなエイリアスで入力する
必要な準備
絵文字 ↔ エイリアスの対応表として、node-emoji から emoji.json を入手し、プロジェクトの App フォルダに配置します。
プログラム
# include <Siv3D.hpp> // OpenSiv3D v0.6.3
// エイリアスと絵文字のペア
struct EmojiAlias
{
// エイリアスとして使う単語
String alias;
// エイリアスに対応する絵文字
String emoji;
};
class EmojiDictionary
{
public:
EmojiDictionary() = default;
explicit EmojiDictionary(FilePathView path)
{
const JSON json = JSON::Load(path);
for (const auto& element : json)
{
const String alias = element.key;
const String emoji = element.value.getString();
m_emojis.push_back(EmojiAlias{ alias, emoji });
m_hashTable.emplace(alias, emoji);
}
// エイリアスの文字数でソート
m_emojis.sort_by([]
(const EmojiAlias& a, const EmojiAlias& b)
{
return a.alias.size() < b.alias.size();
});
}
[[nodiscard]]
explicit operator bool() const noexcept
{
return (not m_emojis.isEmpty());
}
[[nodiscard]]
String getEmoji(StringView alias) const
{
auto it = m_hashTable.find(alias);
if (it == m_hashTable.end())
{
return{};
}
return it->second;
}
[[nodiscard]]
Array<EmojiAlias> getCandidates(const String& emojiAlias, size_t maxCandidates) const
{
if (not emojiAlias)
{
return{};
}
Array<EmojiAlias> candidates;
for (const auto& emoji : m_emojis)
{
if (emoji.alias.includes(emojiAlias))
{
candidates << emoji;
if (maxCandidates <= candidates.size())
{
break;
}
}
}
return candidates;
}
private:
Array<EmojiAlias> m_emojis;
HashTable<String, String> m_hashTable;
};
void Main()
{
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
const Font font{ FontMethod::MSDF, 36, Typeface::Medium };
const Font emojiFont{ 24, Typeface::ColorEmoji };
font.addFallback(emojiFont);
// エイリアスと絵文字のペアを, エイリアスの文字数でソート
// emoji.json は下記から入手
// https://raw.githubusercontent.com/omnidan/node-emoji/master/lib/emoji.json
const EmojiDictionary emojiDictionary{ U"emoji.json" };
if (not emojiDictionary)
{
throw Error{ U"Failed to load emoji.json" };
}
String previousText, text;
String emojiAlias;
// 候補として表示する絵文字の最大個数
constexpr size_t MaxCandidates = 8;
Array<EmojiAlias> candidates;
Optional<size_t> aliasBeginAt;
size_t candidateIndex = 0;
while (System::Update())
{
// テキスト入力の処理
{
TextInput::UpdateText(text, TextInputMode::AllowBackSpace);
if (text != previousText)
{
aliasBeginAt.reset();
for (size_t i = 0; i < text.size(); ++i)
{
const auto ch = text[i];
if (ch == U':')
{
if (not aliasBeginAt)
{
emojiAlias.clear();
aliasBeginAt = i;
}
else
{
if (String emoji = emojiDictionary.getEmoji(emojiAlias))
{
auto& s = text.str();
s.replace((s.begin() + *aliasBeginAt), (s.begin() + i + 1), emoji);
}
emojiAlias.clear();
aliasBeginAt.reset();
}
}
else if (aliasBeginAt)
{
emojiAlias << ch;
}
}
previousText = text;
candidates = emojiDictionary.getCandidates(emojiAlias, MaxCandidates);
candidateIndex = 0;
// デバッグ表示
{
ClearPrint();
Print << U"emojiAlias: " << emojiAlias;
Print << U"aliasBeginAt: " << aliasBeginAt;
}
}
}
// マウスオーバーによる候補の選択
for (auto [i, candidate] : Indexed(candidates))
{
const Rect rect{ 40, (400 - candidates.size() * 40 + i * 40), 720, 38 };
if (rect.mouseOver())
{
candidateIndex = i;
Cursor::RequestStyle(CursorStyle::Hand);
break;
}
}
// キーボードによる候補の選択
if (candidates)
{
if (KeyUp.down())
{
candidateIndex = (candidateIndex + candidates.size() - 1) % candidates.size();
}
else if (KeyDown.down())
{
++candidateIndex %= candidates.size();
}
}
// 候補の表示と処理
for (auto [i, candidate] : Indexed(candidates))
{
const Rect rect{ 40, (400 - candidates.size() * 40 + i * 40), 720, 38 };
const bool selected = (candidateIndex == i);
rect.rounded(4).draw(selected ? ColorF{ 0.7, 0.8, 0.9 } : ColorF{ 0.9 });
emojiFont(candidate.emoji).draw(32, rect.pos.movedBy(10, 4));
font(U':' + candidate.alias + U':').draw(24, rect.pos.movedBy(50, 2), ColorF{ 0.1 });
// 候補がクリックされるか、エンターキーが押されたら
if (rect.leftClicked()
|| (selected && KeyEnter.down()))
{
auto& s = text.str();
s.replace((s.begin() + *aliasBeginAt + 1), s.end(), (candidate.alias + U':'));
break;
}
}
// テキストの表示
{
Rect{ 40, 400, 720, 50 }.draw();
font(text).draw(32, Vec2{ 50, 402 }, ColorF{ 0.1 });
}
}
}
Discussion