1
//! File input button, same as `Button`, but triggers a
2
//! user-supplied path-change callback when clicked
3

            
4
use azul_core::{
5
    callbacks::{CoreCallbackData, Update},
6
    dom::Dom,
7
    refany::RefAny,
8
    resources::OptionImageRef,
9
};
10
use azul_css::{
11
    dynamic_selector::CssPropertyWithConditionsVec,
12
    props::{
13
        basic::*,
14
        layout::*,
15
        property::{CssProperty, *},
16
        style::*,
17
    },
18
    *,
19
};
20

            
21
use crate::{
22
    callbacks::{Callback, CallbackInfo},
23
    widgets::button::{Button, ButtonOnClick, ButtonOnClickCallback},
24
};
25

            
26
#[derive(Debug, Clone, PartialEq)]
27
#[repr(C)]
28
pub struct FileInput {
29
    /// State of the file input
30
    pub file_input_state: FileInputStateWrapper,
31
    /// Default text to display when no file has been selected
32
    /// (default = "Select File...")
33
    pub default_text: AzString,
34

            
35
    /// Optional image that is displayed next to the label
36
    pub image: OptionImageRef,
37
    /// Style for this button container
38
    pub container_style: CssPropertyWithConditionsVec,
39
    /// Style of the label
40
    pub label_style: CssPropertyWithConditionsVec,
41
    /// Style of the image
42
    pub image_style: CssPropertyWithConditionsVec,
43
}
44

            
45
impl Default for FileInput {
46
    fn default() -> Self {
47
        let default_button = Button::create(AzString::from_const_str(""));
48
        Self {
49
            file_input_state: FileInputStateWrapper::default(),
50
            default_text: "Select File...".into(),
51
            image: None.into(),
52
            container_style: default_button.container_style,
53
            label_style: default_button.label_style,
54
            image_style: default_button.image_style,
55
        }
56
    }
57
}
58

            
59
#[derive(Debug, Clone, PartialEq)]
60
#[repr(C)]
61
pub struct FileInputStateWrapper {
62
    pub inner: FileInputState,
63
    pub on_path_change: OptionFileInputOnPathChange,
64
    /// Title displayed in the file selection dialog
65
    pub file_dialog_title: AzString,
66
    /// Default directory of file input
67
    pub default_dir: OptionString,
68
}
69

            
70
impl Default for FileInputStateWrapper {
71
    fn default() -> Self {
72
        Self {
73
            inner: FileInputState::default(),
74
            on_path_change: None.into(),
75
            file_dialog_title: "Select File".into(),
76
            default_dir: None.into(),
77
        }
78
    }
79
}
80

            
81
/// Current state of the file input (selected path)
82
#[derive(Debug, Clone, PartialEq)]
83
#[repr(C)]
84
pub struct FileInputState {
85
    pub path: OptionString,
86
}
87

            
88
impl Default for FileInputState {
89
    fn default() -> Self {
90
        Self { path: None.into() }
91
    }
92
}
93

            
94
/// Callback type invoked when the file input path changes
95
pub type FileInputOnPathChangeCallbackType =
96
    extern "C" fn(RefAny, CallbackInfo, FileInputState) -> Update;
97

            
98
impl_widget_callback!(
99
    FileInputOnPathChange,
100
    OptionFileInputOnPathChange,
101
    FileInputOnPathChangeCallback,
102
    FileInputOnPathChangeCallbackType
103
);
104

            
105
azul_core::impl_managed_callback! {
106
    wrapper:        FileInputOnPathChangeCallback,
107
    info_ty:        CallbackInfo,
108
    return_ty:      Update,
109
    default_ret:    Update::DoNothing,
110
    invoker_static: FILE_INPUT_ON_PATH_CHANGE_INVOKER,
111
    invoker_ty:     AzFileInputOnPathChangeCallbackInvoker,
112
    thunk_fn:       az_file_input_on_path_change_callback_thunk,
113
    setter_fn:      AzApp_setFileInputOnPathChangeCallbackInvoker,
114
    from_handle_fn: AzFileInputOnPathChangeCallback_createFromHostHandle,
115
    extra_args:     [ state: FileInputState ],
116
}
117

            
118
impl FileInput {
119
    pub fn create(path: OptionString) -> Self {
120
        Self {
121
            file_input_state: FileInputStateWrapper {
122
                inner: FileInputState { path },
123
                ..Default::default()
124
            },
125
            ..Default::default()
126
        }
127
    }
128

            
129
    #[inline]
130
    pub fn swap_with_default(&mut self) -> Self {
131
        let mut s = Self::create(None.into());
132
        core::mem::swap(&mut s, self);
133
        s
134
    }
135

            
136
    #[inline]
137
    pub fn set_default_text(&mut self, default_text: AzString) {
138
        self.default_text = default_text;
139
    }
140

            
141
    #[inline]
142
    pub fn with_default_text(mut self, default_text: AzString) -> Self {
143
        self.set_default_text(default_text);
144
        self
145
    }
146

            
147
    #[inline]
148
    pub fn set_on_path_change<I: Into<FileInputOnPathChangeCallback>>(
149
        &mut self,
150
        refany: RefAny,
151
        callback: I,
152
    ) {
153
        self.file_input_state.on_path_change = Some(FileInputOnPathChange {
154
            callback: callback.into(),
155
            refany,
156
        })
157
        .into();
158
    }
159

            
160
    #[inline]
161
    pub fn with_on_path_change<I: Into<FileInputOnPathChangeCallback>>(
162
        mut self,
163
        refany: RefAny,
164
        callback: I,
165
    ) -> Self {
166
        self.set_on_path_change(refany, callback);
167
        self
168
    }
169

            
170
    #[inline]
171
    pub fn dom(self) -> Dom {
172
        // either show the default text or the file name
173
        // including the extension as the button label
174
        let button_label = match self.file_input_state.inner.path.as_ref() {
175
            Some(path) => std::path::Path::new(path.as_str())
176
                .file_name()
177
                .map(|s| s.to_string_lossy().to_string())
178
                .unwrap_or(self.default_text.as_str().to_string())
179
                .into(),
180
            None => self.default_text.clone(),
181
        };
182

            
183
        Button {
184
            label: button_label,
185
            image: self.image,
186
            button_type: crate::widgets::button::ButtonType::Default,
187
            container_style: self.container_style,
188
            label_style: self.label_style,
189
            image_style: self.image_style,
190
            on_click: Some(ButtonOnClick {
191
                refany: RefAny::new(self.file_input_state),
192
                callback: ButtonOnClickCallback {
193
                    cb: fileinput_on_click,
194
                    ctx: azul_core::refany::OptionRefAny::None,
195
                },
196
            })
197
            .into(),
198
        }
199
        .dom()
200
    }
201
}
202

            
203
extern "C" fn fileinput_on_click(mut refany: RefAny, mut info: CallbackInfo) -> Update {
204
    let mut fileinputstatewrapper = match refany.downcast_mut::<FileInputStateWrapper>() {
205
        Some(s) => s,
206
        None => return Update::DoNothing,
207
    };
208
    let fileinputstatewrapper = &mut *fileinputstatewrapper;
209

            
210
    // File dialog is not available in azul_layout
211
    // The user must provide their own file dialog callback via on_path_change
212
    // Just trigger the callback with the current state
213
    let inner = fileinputstatewrapper.inner.clone();
214
    let mut result = match fileinputstatewrapper.on_path_change.as_mut() {
215
        Some(FileInputOnPathChange { refany, callback }) => {
216
            (callback.cb)(refany.clone(), info.clone(), inner)
217
        }
218
        None => return Update::DoNothing,
219
    };
220

            
221
    // Force at least a DOM refresh so the displayed filename updates
222
    result.max_self(Update::RefreshDom);
223

            
224
    result
225
}