Skip to content

Commit 7d1a5d0

Browse files
committed
Add support for label components
1 parent e801f3a commit 7d1a5d0

File tree

5 files changed

+160
-78
lines changed

5 files changed

+160
-78
lines changed

src/builder/create_components.rs

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ impl<const VAL: u8> Serialize for StaticU8<VAL> {
2121
pub enum CreateActionRow<'a> {
2222
Buttons(Cow<'a, [CreateButton<'a>]>),
2323
SelectMenu(CreateSelectMenu<'a>),
24-
/// Only valid in modals!
25-
InputText(CreateInputText<'a>),
2624
}
2725

2826
impl<'a> CreateActionRow<'a> {
@@ -33,10 +31,6 @@ impl<'a> CreateActionRow<'a> {
3331
pub fn select_menu(select_menu: impl Into<CreateSelectMenu<'a>>) -> Self {
3432
Self::SelectMenu(select_menu.into())
3533
}
36-
37-
pub fn input_text(input_text: impl Into<CreateInputText<'a>>) -> Self {
38-
Self::InputText(input_text.into())
39-
}
4034
}
4135

4236
impl serde::Serialize for CreateActionRow<'_> {
@@ -49,7 +43,6 @@ impl serde::Serialize for CreateActionRow<'_> {
4943
match self {
5044
CreateActionRow::Buttons(buttons) => map.serialize_entry("components", &buttons)?,
5145
CreateActionRow::SelectMenu(select) => map.serialize_entry("components", &[select])?,
52-
CreateActionRow::InputText(input) => map.serialize_entry("components", &[input])?,
5346
}
5447

5548
map.end()
@@ -105,6 +98,10 @@ pub enum CreateComponent<'a> {
10598
///
10699
/// A container is a flexible component that can hold multiple nested components.
107100
Container(CreateContainer<'a>),
101+
/// Represents a label component (V2).
102+
///
103+
/// A label is used to hold other components in a modal.
104+
Label(CreateLabel<'a>),
108105
}
109106

110107
/// A builder to create a section component, supports up to a max of **3** components with an
@@ -501,6 +498,54 @@ impl<'a> CreateContainer<'a> {
501498
}
502499
}
503500

501+
/// A builder for creating a label that can hold an [`InputText`] or [`SelectMenu`].
502+
#[derive(Clone, Debug, Serialize)]
503+
#[must_use]
504+
pub struct CreateLabel<'a> {
505+
#[serde(rename = "type")]
506+
kind: StaticU8<18>,
507+
label: Cow<'a, str>,
508+
description: Option<Cow<'a, str>>,
509+
component: CreateLabelComponent<'a>,
510+
}
511+
512+
impl<'a> CreateLabel<'a> {
513+
/// Create a select menu with a specific label.
514+
pub fn select_menu(label: impl Into<Cow<'a, str>>, select_menu: CreateSelectMenu<'a>) -> Self {
515+
Self {
516+
kind: StaticU8::<18>,
517+
label: label.into(),
518+
description: None,
519+
component: CreateLabelComponent::SelectMenu(select_menu),
520+
}
521+
}
522+
523+
/// Create a text input with a specific label.
524+
pub fn input_text(label: impl Into<Cow<'a, str>>, input_text: CreateInputText<'a>) -> Self {
525+
Self {
526+
kind: StaticU8::<18>,
527+
label: label.into(),
528+
description: None,
529+
component: CreateLabelComponent::InputText(input_text),
530+
}
531+
}
532+
533+
/// Sets the description of this component, which will display underneath the label text.
534+
pub fn description(mut self, description: impl Into<Cow<'a, str>>) -> Self {
535+
self.description = Some(description.into());
536+
self
537+
}
538+
}
539+
540+
/// An enum of all valid label components.
541+
#[derive(Clone, Debug, Serialize)]
542+
#[must_use]
543+
#[serde(untagged)]
544+
enum CreateLabelComponent<'a> {
545+
SelectMenu(CreateSelectMenu<'a>),
546+
InputText(CreateInputText<'a>),
547+
}
548+
504549
enum_number! {
505550
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
506551
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
@@ -893,7 +938,6 @@ pub struct CreateInputText<'a> {
893938
kind: ComponentType,
894939
custom_id: Cow<'a, str>,
895940
style: InputTextStyle,
896-
label: Option<Cow<'a, str>>,
897941
min_length: Option<u16>,
898942
max_length: Option<u16>,
899943
required: bool,
@@ -906,14 +950,9 @@ pub struct CreateInputText<'a> {
906950
impl<'a> CreateInputText<'a> {
907951
/// Creates a text input with the given style, label, and custom id (a developer-defined
908952
/// identifier), leaving all other fields empty.
909-
pub fn new(
910-
style: InputTextStyle,
911-
label: impl Into<Cow<'a, str>>,
912-
custom_id: impl Into<Cow<'a, str>>,
913-
) -> Self {
953+
pub fn new(style: InputTextStyle, custom_id: impl Into<Cow<'a, str>>) -> Self {
914954
Self {
915955
style,
916-
label: Some(label.into()),
917956
custom_id: custom_id.into(),
918957

919958
placeholder: None,
@@ -932,12 +971,6 @@ impl<'a> CreateInputText<'a> {
932971
self
933972
}
934973

935-
/// Sets the label of this input text. Replaces the current value as set in [`Self::new`].
936-
pub fn label(mut self, label: impl Into<Cow<'a, str>>) -> Self {
937-
self.label = Some(label.into());
938-
self
939-
}
940-
941974
/// Sets the custom id of the input text, a developer-defined identifier. Replaces the current
942975
/// value as set in [`Self::new`].
943976
pub fn custom_id(mut self, id: impl Into<Cow<'a, str>>) -> Self {

src/builder/create_interaction_response.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use std::collections::HashMap;
33

44
use super::create_poll::Ready;
55
use super::{
6-
CreateActionRow,
76
CreateAllowedMentions,
87
CreateAttachment,
98
CreateComponent,
@@ -424,7 +423,7 @@ impl<'a> CreateAutocompleteResponse<'a> {
424423
#[derive(Clone, Debug, Default, Serialize)]
425424
#[must_use]
426425
pub struct CreateModal<'a> {
427-
components: Cow<'a, [CreateActionRow<'a>]>,
426+
components: Cow<'a, [CreateComponent<'a>]>,
428427
custom_id: Cow<'a, str>,
429428
title: Cow<'a, str>,
430429
}
@@ -442,7 +441,7 @@ impl<'a> CreateModal<'a> {
442441
/// Sets the components of this message.
443442
///
444443
/// Overwrites existing components.
445-
pub fn components(mut self, components: impl Into<Cow<'a, [CreateActionRow<'a>]>>) -> Self {
444+
pub fn components(mut self, components: impl Into<Cow<'a, [CreateComponent<'a>]>>) -> Self {
446445
self.components = components.into();
447446
self
448447
}

src/collector/quick_modal.rs

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
use std::borrow::Cow;
22

3-
use crate::builder::{CreateActionRow, CreateInputText, CreateInteractionResponse, CreateModal};
3+
use crate::builder::{
4+
CreateComponent,
5+
CreateInputText,
6+
CreateInteractionResponse,
7+
CreateLabel,
8+
CreateModal,
9+
CreateTextDisplay,
10+
};
411
use crate::collector::ModalInteractionCollector;
512
use crate::gateway::client::Context;
613
use crate::internal::prelude::*;
@@ -31,15 +38,15 @@ pub struct QuickModalResponse {
3138
pub struct CreateQuickModal<'a> {
3239
title: Cow<'a, str>,
3340
timeout: Option<std::time::Duration>,
34-
input_texts: Vec<CreateInputText<'a>>,
41+
components: Vec<CreateComponent<'a>>,
3542
}
3643

3744
impl<'a> CreateQuickModal<'a> {
3845
pub fn new(title: impl Into<Cow<'a, str>>) -> Self {
3946
Self {
4047
title: title.into(),
4148
timeout: None,
42-
input_texts: Vec::new(),
49+
components: Vec::new(),
4350
}
4451
}
4552

@@ -52,27 +59,40 @@ impl<'a> CreateQuickModal<'a> {
5259
self
5360
}
5461

62+
/// Adds a text display field.
63+
pub fn text(mut self, content: impl Into<Cow<'a, str>>) -> Self {
64+
self.components.push(CreateComponent::TextDisplay(CreateTextDisplay::new(content)));
65+
self
66+
}
67+
5568
/// Adds an input text field.
56-
///
57-
/// As the `custom_id` field of [`CreateInputText`], just supply an empty string. All custom
58-
/// IDs are overwritten by [`CreateQuickModal`] when sending the modal.
59-
pub fn field(mut self, input_text: CreateInputText<'a>) -> Self {
60-
self.input_texts.push(input_text);
69+
pub fn field(
70+
mut self,
71+
label: impl Into<Cow<'a, str>>,
72+
input_text: CreateInputText<'a>,
73+
) -> Self {
74+
self.components.push(CreateComponent::Label(
75+
CreateLabel::input_text(label, input_text).description("test"),
76+
));
6177
self
6278
}
6379

6480
/// Convenience method to add a single-line input text field.
6581
///
6682
/// Wraps [`Self::field`].
6783
pub fn short_field(self, label: impl Into<Cow<'a, str>>) -> Self {
68-
self.field(CreateInputText::new(InputTextStyle::Short, label, ""))
84+
let input_text =
85+
CreateInputText::new(InputTextStyle::Short, self.components.len().to_string());
86+
self.field(label, input_text)
6987
}
7088

7189
/// Convenience method to add a multi-line input text field.
7290
///
7391
/// Wraps [`Self::field`].
7492
pub fn paragraph_field(self, label: impl Into<Cow<'a, str>>) -> Self {
75-
self.field(CreateInputText::new(InputTextStyle::Paragraph, label, ""))
93+
let input_text =
94+
CreateInputText::new(InputTextStyle::Paragraph, self.components.len().to_string());
95+
self.field(label, input_text)
7696
}
7797

7898
/// # Errors
@@ -84,22 +104,13 @@ impl<'a> CreateQuickModal<'a> {
84104
interaction_id: InteractionId,
85105
token: &str,
86106
) -> Result<Option<QuickModalResponse>, crate::Error> {
87-
let modal_custom_id = interaction_id.to_arraystring();
88107
let builder = CreateInteractionResponse::Modal(
89-
CreateModal::new(modal_custom_id.as_str(), self.title).components(
90-
self.input_texts
91-
.into_iter()
92-
.enumerate()
93-
.map(|(i, input_text)| {
94-
CreateActionRow::InputText(input_text.custom_id(i.to_string()))
95-
})
96-
.collect::<Vec<_>>(),
97-
),
108+
CreateModal::new(interaction_id.to_string(), self.title).components(self.components),
98109
);
99110
builder.execute(&ctx.http, interaction_id, token).await?;
100111

101112
let collector = ModalInteractionCollector::new(ctx)
102-
.custom_ids(vec![FixedString::from_str_trunc(&modal_custom_id)]);
113+
.custom_ids(vec![FixedString::from_str_trunc(&interaction_id.to_string())]);
103114

104115
let collector = match self.timeout {
105116
Some(timeout) => collector.timeout(timeout),
@@ -114,23 +125,22 @@ impl<'a> CreateQuickModal<'a> {
114125
.data
115126
.components
116127
.iter()
117-
.filter_map(|row| match row.components.first() {
118-
Some(ActionRowComponent::InputText(text)) => {
128+
.filter_map(|component| {
129+
if let Component::Label(label) = component
130+
&& let LabelComponent::InputText(text) = &label.component
131+
{
119132
if let Some(value) = &text.value {
120133
Some(value.clone())
121134
} else {
122135
tracing::warn!("input text value was empty in modal response");
123136
None
124137
}
125-
},
126-
Some(other) => {
127-
tracing::warn!("expected input text in modal response, got {:?}", other);
128-
None
129-
},
130-
None => {
131-
tracing::warn!("empty action row");
138+
} else {
139+
if !matches!(component, Component::TextDisplay(_)) {
140+
tracing::warn!("expected input text in modal response, got {component:?}");
141+
}
132142
None
133-
},
143+
}
134144
})
135145
.collect();
136146

0 commit comments

Comments
 (0)