🐱

Siv3D | 絵文字を :cat: のようなエイリアスで入力する

2022/01/17に公開

絵文字を :cat: のようなエイリアスで入力する

https://youtu.be/kQBUVXAFTj8

必要な準備

絵文字 ↔ エイリアスの対応表として、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