Skip to main content

ui/components/
file_input.rs

1use gpui::{AnyElement, ClickEvent, ElementId, IntoElement};
2
3use crate::prelude::*;
4
5/// A styled file-upload trigger: dashed `semantic::border`, an icon, and a
6/// "Click to upload" label.
7///
8/// LIMITATION (real scope boundary, not a cut corner): this codebase has no
9/// OS file-dialog API — no bindings exist in `gpui`/`gpui_platform` for
10/// opening a native file picker. `FileInput` is a purely presentational
11/// trigger; clicking it only invokes `on_click`. It performs no file I/O and
12/// does not open any dialog. The caller is responsible for wiring
13/// `on_click` to their own file-dialog integration when one becomes
14/// available.
15#[derive(IntoElement, RegisterComponent)]
16pub struct FileInput {
17    id: ElementId,
18    label: SharedString,
19    hint: Option<SharedString>,
20    disabled: bool,
21    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
22}
23
24impl FileInput {
25    pub fn new(id: impl Into<ElementId>) -> Self {
26        Self {
27            id: id.into(),
28            label: "Click to upload".into(),
29            hint: None,
30            disabled: false,
31            on_click: None,
32        }
33    }
34
35    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
36        self.label = label.into();
37        self
38    }
39
40    pub fn hint(mut self, hint: impl Into<SharedString>) -> Self {
41        self.hint = Some(hint.into());
42        self
43    }
44
45    pub fn disabled(mut self, disabled: bool) -> Self {
46        self.disabled = disabled;
47        self
48    }
49
50    /// Sets the click handler. Caller wires this to their own file-dialog
51    /// integration — see the type-level doc comment for the scope boundary.
52    pub fn on_click(
53        mut self,
54        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
55    ) -> Self {
56        self.on_click = Some(Box::new(handler));
57        self
58    }
59}
60
61impl RenderOnce for FileInput {
62    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
63        v_flex()
64            .id(self.id)
65            .items_center()
66            .justify_center()
67            .gap_2()
68            .w_full()
69            .py_8()
70            .rounded_lg()
71            .border_2()
72            .border_dashed()
73            .border_color(semantic::border(cx))
74            .when(!self.disabled, |this| this.cursor_pointer())
75            .when(self.disabled, |this| this.opacity(0.5))
76            .child(
77                Icon::new(IconName::Paperclip)
78                    .size(IconSize::Medium)
79                    .color(Color::Muted),
80            )
81            .child(Label::new(self.label).size(LabelSize::Small))
82            .when_some(self.hint, |this, hint| {
83                this.child(Label::new(hint).size(LabelSize::XSmall).color(Color::Muted))
84            })
85            .when_some(self.on_click.filter(|_| !self.disabled), |this, handler| {
86                this.on_click(handler)
87            })
88    }
89}
90
91impl Component for FileInput {
92    fn scope() -> ComponentScope {
93        ComponentScope::Input
94    }
95
96    fn description() -> Option<&'static str> {
97        Some(
98            "A styled file-upload trigger. No OS file-dialog API exists in this codebase — \
99             purely presentational, caller wires `on_click` to their own integration.",
100        )
101    }
102
103    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
104        Some(
105            v_flex()
106                .gap_4()
107                .child(
108                    FileInput::new("file-input-default")
109                        .hint("PNG, JPG up to 10MB")
110                        .on_click(|_, _, _| {})
111                        .into_any_element(),
112                )
113                .child(
114                    FileInput::new("file-input-selected")
115                        .label("resume.pdf")
116                        .hint("245 KB — click to replace")
117                        .on_click(|_, _, _| {})
118                        .into_any_element(),
119                )
120                .child(
121                    FileInput::new("file-input-disabled")
122                        .disabled(true)
123                        .into_any_element(),
124                )
125                .into_any_element(),
126        )
127    }
128}