titanium_model/
component.rs

1//! Message Components (Buttons, Select Menus, etc.)
2
3use crate::reaction::ReactionEmoji;
4// Imports removed
5
6use crate::TitanString;
7use serde::{Deserialize, Serialize};
8
9/// Top-level component type.
10///
11/// In messages, this is usually an `ActionRow`.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(untagged)]
14pub enum Component<'a> {
15    /// An action row containing other components.
16    ActionRow(ActionRow<'a>),
17    /// A button component (only valid inside ActionRow).
18    Button(Button<'a>),
19    /// A select menu (only valid inside ActionRow).
20    SelectMenu(SelectMenu<'a>),
21    /// A text input (only valid in modals).
22    TextInput(TextInput<'a>),
23}
24
25/// The type of component.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(from = "u8", into = "u8")]
28pub enum ComponentType {
29    /// Container for other components.
30    ActionRow = 1,
31    /// Button object.
32    Button = 2,
33    /// Select menu for picking text.
34    StringSelect = 3,
35    /// Text input object.
36    TextInput = 4,
37    /// Select menu for users.
38    UserSelect = 5,
39    /// Select menu for roles.
40    RoleSelect = 6,
41    /// Select menu for mentionables (users + roles).
42    MentionableSelect = 7,
43    /// Select menu for channels.
44    ChannelSelect = 8,
45}
46
47impl From<u8> for ComponentType {
48    fn from(value: u8) -> Self {
49        match value {
50            1 => ComponentType::ActionRow,
51            2 => ComponentType::Button,
52            3 => ComponentType::StringSelect,
53            4 => ComponentType::TextInput,
54            5 => ComponentType::UserSelect,
55            6 => ComponentType::RoleSelect,
56            7 => ComponentType::MentionableSelect,
57            8 => ComponentType::ChannelSelect,
58            _ => ComponentType::ActionRow, // Fallback
59        }
60    }
61}
62
63impl From<ComponentType> for u8 {
64    fn from(value: ComponentType) -> Self {
65        value as u8
66    }
67}
68
69/// A container for other components.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ActionRow<'a> {
72    /// Type 1 (ActionRow).
73    #[serde(rename = "type")]
74    pub component_type: ComponentType,
75    /// List of child components.
76    pub components: Vec<Component<'a>>,
77}
78
79impl<'a> Default for ActionRow<'a> {
80    fn default() -> Self {
81        Self {
82            component_type: ComponentType::ActionRow,
83            components: Vec::new(),
84        }
85    }
86}
87
88/// A clickable button.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct Button<'a> {
91    /// Type 2 (Button).
92    #[serde(rename = "type")]
93    pub component_type: ComponentType,
94    /// Style of the button.
95    pub style: ButtonStyle,
96    /// Text label (max 80 characters).
97    #[serde(default)]
98    pub label: Option<TitanString<'a>>,
99    /// Emoji to display.
100    #[serde(default)]
101    pub emoji: Option<ReactionEmoji<'a>>,
102    /// Custom ID (max 100 chars). Required for non-link buttons.
103    #[serde(default)]
104    pub custom_id: Option<TitanString<'a>>,
105    /// URL for link buttons.
106    #[serde(default)]
107    pub url: Option<TitanString<'a>>,
108    /// Whether the button is disabled.
109    #[serde(default)]
110    pub disabled: bool,
111}
112
113impl<'a> Button<'a> {
114    /// Create a builder for a Button.
115    pub fn builder(
116        custom_id: impl Into<TitanString<'a>>,
117        style: ButtonStyle,
118    ) -> crate::builder::ButtonBuilder<'a> {
119        crate::builder::ButtonBuilder::new()
120            .custom_id(custom_id)
121            .style(style)
122    }
123
124    /// Create a builder for a Link Button.
125    pub fn builder_link(url: impl Into<TitanString<'a>>) -> crate::builder::ButtonBuilder<'a> {
126        crate::builder::ButtonBuilder::new()
127            .url(url)
128            .style(ButtonStyle::Link)
129    }
130}
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(from = "u8", into = "u8")]
133pub enum ButtonStyle {
134    /// Blurple (Primary).
135    Primary = 1,
136    /// Grey (Secondary).
137    Secondary = 2,
138    /// Green (Success).
139    Success = 3,
140    /// Red (Danger).
141    Danger = 4,
142    /// Grey Link (Link).
143    Link = 5,
144}
145
146impl From<u8> for ButtonStyle {
147    fn from(value: u8) -> Self {
148        match value {
149            1 => ButtonStyle::Primary,
150            2 => ButtonStyle::Secondary,
151            3 => ButtonStyle::Success,
152            4 => ButtonStyle::Danger,
153            5 => ButtonStyle::Link,
154            _ => ButtonStyle::Primary,
155        }
156    }
157}
158
159impl From<ButtonStyle> for u8 {
160    fn from(value: ButtonStyle) -> Self {
161        value as u8
162    }
163}
164
165/// A menu for selecting items.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct SelectMenu<'a> {
168    /// Type (3, 5, 6, 7, 8).
169    #[serde(rename = "type")]
170    pub component_type: ComponentType,
171    /// Custom ID to identify the menu.
172    pub custom_id: TitanString<'a>,
173    /// Options (only for StringSelect).
174    #[serde(default)]
175    pub options: Vec<SelectOption<'a>>,
176    /// Placeholder text.
177    #[serde(default)]
178    pub placeholder: Option<TitanString<'a>>,
179    /// Minimum values to append.
180    #[serde(default)]
181    pub min_values: Option<u8>,
182    /// Maximum values to append.
183    #[serde(default)]
184    pub max_values: Option<u8>,
185    /// Whether the menu is disabled.
186    #[serde(default)]
187    pub disabled: bool,
188}
189
190impl<'a> SelectMenu<'a> {
191    /// Create a builder for a SelectMenu.
192    pub fn builder(custom_id: impl Into<TitanString<'a>>) -> crate::builder::SelectMenuBuilder<'a> {
193        crate::builder::SelectMenuBuilder::new(custom_id)
194    }
195}
196
197/// An option in a string select menu.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct SelectOption<'a> {
200    /// The user-facing name of the option.
201    pub label: TitanString<'a>,
202    /// The dev-facing value of the option.
203    pub value: TitanString<'a>,
204    /// Additional description.
205    #[serde(default)]
206    pub description: Option<TitanString<'a>>,
207    /// Emoji for the option.
208    #[serde(default)]
209    pub emoji: Option<ReactionEmoji<'a>>,
210    /// Whether this option is selected by default.
211    #[serde(default)]
212    pub default: bool,
213}
214
215/// A text input field (Modals only).
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct TextInput<'a> {
218    /// Type 4 (TextInput).
219    #[serde(rename = "type")]
220    pub component_type: ComponentType,
221    /// Custom ID.
222    pub custom_id: TitanString<'a>,
223    /// Input style (Short vs Paragraph).
224    pub style: TextInputStyle,
225    /// Label for the input.
226    pub label: TitanString<'a>,
227    /// Minimum length.
228    #[serde(default)]
229    pub min_length: Option<u16>,
230    /// Maximum length.
231    #[serde(default)]
232    pub max_length: Option<u16>,
233    /// Whether required.
234    #[serde(default)]
235    pub required: Option<bool>,
236    /// Pre-filled value.
237    #[serde(default)]
238    pub value: Option<TitanString<'a>>,
239    /// Placeholder text.
240    #[serde(default)]
241    pub placeholder: Option<TitanString<'a>>,
242}
243
244/// Text Input Style.
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(from = "u8", into = "u8")]
247pub enum TextInputStyle {
248    /// Single-line input.
249    Short = 1,
250    /// Multi-line input.
251    Paragraph = 2,
252}
253
254impl From<u8> for TextInputStyle {
255    fn from(value: u8) -> Self {
256        match value {
257            1 => TextInputStyle::Short,
258            2 => TextInputStyle::Paragraph,
259            _ => TextInputStyle::Short,
260        }
261    }
262}
263
264impl From<TextInputStyle> for u8 {
265    fn from(value: TextInputStyle) -> Self {
266        value as u8
267    }
268}