Embeds & Components
The Discord bot uses Heimdall's brand colors for embeds and provides builders for common UI patterns.
Heimdall Color Scheme
// src/utils/colors.rs
use serenity::model::Color;
// Brand colors are declared as consts at the module root (referenced as `colors::BRAND`).
/// Primary brand color - Mint green (#14f5bf)
pub const BRAND: Color = Color::new(0x14f5bf);
/// Error color - Red (#ff5859)
pub const ERROR: Color = Color::new(0xff5859);
/// Warning color - Amber (#f59e0b)
pub const WARNING: Color = Color::new(0xf59e0b);
/// Info color - Blue (#3b82f6)
pub const INFO: Color = Color::new(0x3b82f6);
/// Success color - Green (#10b981)
pub const SUCCESS: Color = Color::new(0x10b981);
/// Neutral color - Gray (#9ca3af)
pub const NEUTRAL: Color = Color::new(0x9ca3af);
/// Raw u32 values for non-Serenity usage
pub mod colors_raw {
pub const BRAND: u32 = 0x14f5bf;
pub const ERROR: u32 = 0xff5859;
pub const WARNING: u32 = 0xf59e0b;
pub const INFO: u32 = 0x3b82f6;
pub const SUCCESS: u32 = 0x10b981;
pub const NEUTRAL: u32 = 0x9ca3af;
}
Embed Builders
Standard Heimdall Embed
// src/embeds/builders.rs
use serenity::all::{CreateEmbed, CreateEmbedFooter};
use crate::utils::colors;
use rust_i18n::t;
/// Build a standard Smutje embed with brand color
pub fn smutje_embed() -> CreateEmbed {
CreateEmbed::new()
.color(colors::BRAND)
.footer(CreateEmbedFooter::new(t!("embeds.footer")))
}
/// Build an error embed (title gets a ❌ prefix)
pub fn error_embed(title: &str, description: &str) -> CreateEmbed {
CreateEmbed::new()
.color(colors::ERROR)
.title(format!("❌ {}", title))
.description(description)
.footer(CreateEmbedFooter::new(t!("embeds.footer")))
}
/// Build a success embed (title gets a ✅ prefix)
pub fn success_embed(title: &str, description: &str) -> CreateEmbed {
CreateEmbed::new()
.color(colors::SUCCESS)
.title(format!("✅ {}", title))
.description(description)
.footer(CreateEmbedFooter::new(t!("embeds.footer")))
}
/// Build a warning embed (title gets a ⚠️ prefix)
pub fn warning_embed(title: &str, description: &str) -> CreateEmbed {
CreateEmbed::new()
.color(colors::WARNING)
.title(format!("⚠️ {}", title))
.description(description)
.footer(CreateEmbedFooter::new(t!("embeds.footer")))
}
/// Build an info embed (title gets an ℹ️ prefix)
pub fn info_embed(title: &str, description: &str) -> CreateEmbed {
CreateEmbed::new()
.color(colors::INFO)
.title(format!("ℹ️ {}", title))
.description(description)
.footer(CreateEmbedFooter::new(t!("embeds.footer")))
}
Note: The status builders (
error_embed,success_embed,warning_embed,info_embed) automatically prepend an emoji to the title (❌ ✅ ⚠️ ℹ️).smutje_embed()andneutral_embed()do not.
Usage Examples
use crate::embeds::builders::*;
use serenity::all::CreateMessage;
// Success message
let embed = success_embed("Action Completed", "The user has been updated successfully.");
msg.channel_id.send_message(ctx, CreateMessage::new().embed(embed)).await?;
// Error message
let embed = error_embed("Error", "Failed to process the request.");
msg.channel_id.send_message(ctx, CreateMessage::new().embed(embed)).await?;
// Custom embed with fields
let embed = smutje_embed()
.title("Bot Information")
.field("Version", env!("CARGO_PKG_VERSION"), true)
.field("Uptime", "2 hours", true)
.field("Guilds", "150", true)
.thumbnail("https://example.com/logo.png");
msg.channel_id.send_message(ctx, CreateMessage::new().embed(embed)).await?;
Embed Templates
src/embeds/templates.rs provides pre-defined templates: help_template, info_template, moderation_template, permission_denied_template, and not_linked_template.
// src/embeds/templates.rs
use serenity::all::CreateEmbed;
use crate::utils::colors;
use rust_i18n::t;
/// Template for help command embeds
pub fn help_template(commands: Vec<(&str, &str)>) -> CreateEmbed {
let mut embed = CreateEmbed::new()
.color(colors::INFO)
.title(t!("commands.help.title"));
for (name, description) in commands {
embed = embed.field(name, description, false);
}
embed
}
/// Template for bot info embed
pub fn info_template(version: &str, uptime: &str, guilds: usize) -> CreateEmbed {
CreateEmbed::new()
.color(colors::BRAND)
.title(t!("commands.info.title"))
.field(t!("commands.info.version"), version, true)
.field(t!("commands.info.uptime"), uptime, true)
.field(t!("commands.info.guilds"), guilds.to_string(), true)
}
/// Template for moderation action embed
pub fn moderation_template(action: &str, target: &str, moderator: &str, reason: &str) -> CreateEmbed {
CreateEmbed::new()
.color(colors::WARNING)
.title(format!("🔨 Moderation: {}", action))
.field("Target", target, true)
.field("Moderator", moderator, true)
.field("Reason", reason, false)
}
/// Template for permission denied embed
pub fn permission_denied_template() -> CreateEmbed {
CreateEmbed::new()
.color(colors::ERROR)
.title("❌ Permission Denied")
.description(t!("errors.permission_denied"))
}
/// Template for not linked embed
pub fn not_linked_template() -> CreateEmbed {
CreateEmbed::new()
.color(colors::WARNING)
.title("🔗 Account Not Linked")
.description(t!("errors.not_linked"))
}
Note:
user_profile_embedandconfirmation_embedlive insrc/embeds/builders.rs, not intemplates.rs.
Button Builders
// src/components/buttons.rs
use serenity::all::{CreateButton, ButtonStyle, CreateActionRow};
/// Primary action button
pub fn primary_button(custom_id: &str, label: &str) -> CreateButton {
CreateButton::new(custom_id)
.label(label)
.style(ButtonStyle::Primary)
}
/// Secondary action button
pub fn secondary_button(custom_id: &str, label: &str) -> CreateButton {
CreateButton::new(custom_id)
.label(label)
.style(ButtonStyle::Secondary)
}
/// Success button
pub fn success_button(custom_id: &str, label: &str) -> CreateButton {
CreateButton::new(custom_id)
.label(label)
.style(ButtonStyle::Success)
}
/// Danger button
pub fn danger_button(custom_id: &str, label: &str) -> CreateButton {
CreateButton::new(custom_id)
.label(label)
.style(ButtonStyle::Danger)
}
/// Link button (opens URL)
pub fn link_button(url: &str, label: &str) -> CreateButton {
CreateButton::new_link(url)
.label(label)
}
/// Create a row of buttons
pub fn button_row(buttons: Vec<CreateButton>) -> CreateActionRow {
CreateActionRow::Buttons(buttons)
}
Button Usage
use crate::components::buttons::*;
use serenity::all::CreateMessage;
// Confirmation buttons
let confirm = success_button("confirm_action", "Confirm");
let cancel = danger_button("cancel_action", "Cancel");
let msg = CreateMessage::new()
.content("Are you sure?")
.components(vec![button_row(vec![confirm, cancel])]);
channel.send_message(ctx, msg).await?;
Select Menu Builders
// src/components/selects.rs
use serenity::all::{
CreateSelectMenu, CreateSelectMenuKind, CreateSelectMenuOption,
CreateActionRow,
};
/// Create a string select menu
pub fn string_select(
custom_id: &str,
placeholder: &str,
options: Vec<(&str, &str)>,
) -> CreateSelectMenu {
let options: Vec<CreateSelectMenuOption> = options
.into_iter()
.map(|(label, value)| CreateSelectMenuOption::new(label, value))
.collect();
CreateSelectMenu::new(custom_id, CreateSelectMenuKind::String { options })
.placeholder(placeholder)
}
/// Create a user select menu
pub fn user_select(custom_id: &str, placeholder: &str) -> CreateSelectMenu {
CreateSelectMenu::new(custom_id, CreateSelectMenuKind::User)
.placeholder(placeholder)
}
/// Create a role select menu
pub fn role_select(custom_id: &str, placeholder: &str) -> CreateSelectMenu {
CreateSelectMenu::new(custom_id, CreateSelectMenuKind::Role)
.placeholder(placeholder)
}
/// Create a select menu row
pub fn select_row(select: CreateSelectMenu) -> CreateActionRow {
CreateActionRow::SelectMenu(select)
}
Select Menu Usage
use crate::components::selects::*;
use serenity::all::CreateMessage;
// Language selection
let select = string_select(
"language_select",
"Select a language",
vec![
("English", "en"),
("German", "de"),
],
);
let msg = CreateMessage::new()
.content("Choose your language:")
.components(vec![select_row(select)]);
channel.send_message(ctx, msg).await?;
Handling Component Interactions
// src/interactions/components.rs
use serenity::all::{
ComponentInteraction, CreateInteractionResponse,
CreateInteractionResponseMessage,
};
pub async fn handle_component(
ctx: &Context,
interaction: ComponentInteraction,
) -> Result<(), Error> {
match interaction.data.custom_id.as_str() {
"confirm_action" => {
interaction.create_response(ctx, CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content("Action confirmed!")
.ephemeral(true)
)).await?;
}
"cancel_action" => {
interaction.create_response(ctx, CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content("Action cancelled.")
.ephemeral(true)
)).await?;
}
"language_select" => {
if let ComponentInteractionDataKind::StringSelect { values } = &interaction.data.kind {
let selected = &values[0];
rust_i18n::set_locale(selected);
// Save to user preferences...
}
}
_ => {}
}
Ok(())
}
Message Builders
Safe text construction using Serenity's MessageBuilder:
use serenity::utils::MessageBuilder;
// Safe mention
let content = MessageBuilder::new()
.push("Hello, ")
.mention(&user)
.push("! Welcome to ")
.push_bold_safe(&guild_name)
.push(".")
.build();
// Code block
let content = MessageBuilder::new()
.push("Here's the code:\n")
.push_codeblock_safe(code, Some("rust"))
.build();
// Quote
let content = MessageBuilder::new()
.quote_rest()
.push(quote_text)
.build();