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