Skip to content

User interface

Plugins do not draw anything themselves. They return descriptions of UI, and PhoenixCord renders them with Discord’s own components and theme.

An element is a tree of nodes. WIT cannot express recursive types, so the tree is stored flat: nodes[0] is the root and each node lists its children by index. In Rust, build a View and flatten it:

use phoenixcord_sdk::View;
use phoenixcord_sdk::types::{Color, ColorToken, Direction, Icon, Style, TextSize};
let badge = View::stack(vec![
View::icon(Icon::Named("clock".into())),
View::text("3s late"),
])
.style(Style {
direction: Some(Direction::Row),
gap: Some(4.0),
foreground: Some(Color::Token(ColorToken::TextMuted)),
text_size: Some(TextSize::Xs),
..Style::default()
})
.tooltip("Message latency")
.into_element();

Node kinds are stack, text, icon, image, button, toggle, text-input, select, link, code, divider and spinner. Use color-tokens instead of raw colors so plugin UI follows the user’s theme. Icons are either named icons from Discord’s icon set or raw svg markup, where currentColor follows the node’s foreground color.

Interactive nodes, menu items and buttons carry an action string. When the user interacts, the plugin receives event.action with that string and, for toggles, inputs and selects, the new value:

fn on_event(&mut self, event: Event) {
if let Event::Action(action) = event
&& action.action == "toggle"
{
self.enabled = !self.enabled;
host::request_render();
}
}
fn chat_bar_buttons(&mut self, context: ChatBarContext) -> Vec<ChatBarButton> {
if !context.main_chat {
return Vec::new();
}
vec![ChatBarButton {
tooltip: "Toggle".into(),
icon: Icon::Named("bell".into()),
action: "toggle".into(),
active: self.enabled,
}]
}

context-menu receives a menu-target: a user, message, channel, guild, role, image, link or the message input. Return items to add to that menu. Items with a parent index form a submenu under that item.

fn context_menu(&mut self, target: MenuTarget) -> Vec<MenuItem> {
let MenuTarget::User(user) = target else {
return Vec::new();
};
vec![MenuItem {
label: "Copy User URL".into(),
action: format!("copy-user-url:{}", user.user_id),
icon: Some(Icon::Named("link".into())),
checked: None,
danger: false,
parent: None,
}]
}
Hook Returns
profile-sections Elements added to a user profile
user-badges badges shown on a profile
member-decorations Elements next to a user in the member list and DM list
server-list-elements Elements added to the server list
Function Shows
host::show_toast A short message at the bottom of the window
host::show_notice A bar at the top of the client, with an optional button
host::notify A desktop notification
host::open_modal A modal containing an element. Update it with update_modal, close it with close_modal