Skip to main content

ragrig_gpui/
attachments.rs

1//! Composer attachment chips.
2
3use std::rc::Rc;
4
5use gpui_kit::{
6    AnyElement, App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _,
7    RenderOnce, StatefulInteractiveElement as _, Styled as _, Window,
8    assets::IconName,
9    component::{
10        ActiveTheme as _, Disableable as _, Sizable as _,
11        button::{Button, ButtonVariants as _},
12        h_flex,
13    },
14    div,
15};
16
17use crate::model::{Attachment, AttachmentKind};
18
19/// What the user did with an attachment chip.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum AttachmentEvent {
22    /// A toggle chip was clicked; the host flips its state.
23    Toggled(String),
24    /// The drop button was clicked.
25    Dropped(String),
26    /// An offer's action button was clicked.
27    Accepted(String),
28}
29
30type AttachmentHandler = Rc<dyn Fn(AttachmentEvent, &mut Window, &mut App)>;
31
32/// A wrapping row of [`Attachment`] chips.
33///
34/// The builder is presentational: it reports interactions through
35/// [`on_event`](Self::on_event) and never mutates the attachments.
36#[derive(IntoElement)]
37pub struct AttachmentRows {
38    attachments: Vec<Attachment>,
39    on_event: Option<AttachmentHandler>,
40}
41
42impl AttachmentRows {
43    /// Render `attachments` left to right, wrapping when needed.
44    pub fn new(attachments: impl IntoIterator<Item = Attachment>) -> Self {
45        Self {
46            attachments: attachments.into_iter().collect(),
47            on_event: None,
48        }
49    }
50
51    /// Receive chip interactions.
52    pub fn on_event(
53        mut self,
54        handler: impl Fn(AttachmentEvent, &mut Window, &mut App) + 'static,
55    ) -> Self {
56        self.on_event = Some(Rc::new(handler));
57        self
58    }
59}
60
61impl RenderOnce for AttachmentRows {
62    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
63        let handler = self.on_event;
64        h_flex().flex_wrap().gap_2().children(
65            self.attachments
66                .into_iter()
67                .enumerate()
68                .map(|(index, attachment)| chip(index, attachment, handler.clone(), cx)),
69        )
70    }
71}
72
73fn chip(
74    index: usize,
75    attachment: Attachment,
76    handler: Option<AttachmentHandler>,
77    cx: &mut App,
78) -> AnyElement {
79    let theme = cx.theme();
80    let on = attachment.is_on();
81    let enabled = attachment.enabled;
82    let id = attachment.id.clone();
83    let element_id = ElementId::Name(format!("attachment-{index}-{id}").into());
84
85    let mut row = h_flex()
86        .id(element_id)
87        .items_center()
88        .gap_1()
89        .px_2()
90        .py_1()
91        .rounded(theme.radius)
92        .border_1()
93        .border_color(if on { theme.primary } else { theme.border })
94        .bg(if on {
95            theme.primary.opacity(0.08)
96        } else {
97            theme.background
98        })
99        .text_sm()
100        .text_color(if enabled {
101            theme.foreground
102        } else {
103            theme.muted_foreground
104        })
105        .child(
106            div()
107                .text_color(if on {
108                    theme.primary
109                } else {
110                    theme.muted_foreground
111                })
112                .child(attachment.icon),
113        )
114        .child(attachment.label.clone());
115
116    match &attachment.kind {
117        AttachmentKind::Toggle { .. } => {
118            if enabled {
119                let handler = handler.clone();
120                let id = id.clone();
121                row = row.cursor_pointer().on_click(move |_, window, cx| {
122                    if let Some(handler) = &handler {
123                        handler(AttachmentEvent::Toggled(id.clone()), window, cx);
124                    }
125                });
126            }
127        }
128        AttachmentKind::Pending => {
129            row = row.child(drop_button(index, &id, enabled, handler.clone()));
130        }
131        AttachmentKind::Offer { action } => {
132            let action_id = ElementId::Name(format!("attachment-offer-{index}-{id}").into());
133            let accept_handler = handler.clone();
134            let accept_id = id.clone();
135            row = row
136                .child(
137                    Button::new(action_id)
138                        .label(action.clone())
139                        .xsmall()
140                        .ghost()
141                        .disabled(!enabled)
142                        .on_click(move |_, window, cx| {
143                            if let Some(handler) = &accept_handler {
144                                handler(AttachmentEvent::Accepted(accept_id.clone()), window, cx);
145                            }
146                        }),
147                )
148                .child(drop_button(index, &id, enabled, handler));
149        }
150    }
151
152    row.into_any_element()
153}
154
155fn drop_button(
156    index: usize,
157    id: &str,
158    enabled: bool,
159    handler: Option<AttachmentHandler>,
160) -> Button {
161    let element_id = ElementId::Name(format!("attachment-drop-{index}-{id}").into());
162    let drop_id = id.to_owned();
163    Button::new(element_id)
164        .icon(IconName::Close)
165        .xsmall()
166        .ghost()
167        .disabled(!enabled)
168        .tooltip("Drop")
169        .on_click(move |_, window, cx| {
170            if let Some(handler) = &handler {
171                handler(AttachmentEvent::Dropped(drop_id.clone()), window, cx);
172            }
173        })
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn events_name_the_attachment() {
182        assert_eq!(
183            AttachmentEvent::Toggled("draft".to_owned()),
184            AttachmentEvent::Toggled("draft".to_owned())
185        );
186        assert_ne!(
187            AttachmentEvent::Dropped("a".to_owned()),
188            AttachmentEvent::Accepted("a".to_owned())
189        );
190    }
191
192    #[test]
193    fn a_row_accepts_any_iterator() {
194        let rows = AttachmentRows::new([
195            Attachment::document("draft", "draft.md"),
196            Attachment::pending("region", "Selection"),
197        ]);
198        assert_eq!(rows.attachments.len(), 2);
199    }
200}