Skip to main content

azul_layout/widgets/
file_input.rs

1//! File input button, same as `Button`, but triggers a
2//! user-supplied path-change callback when clicked
3
4use azul_core::{
5    callbacks::{CoreCallbackData, Update},
6    dom::Dom,
7    refany::RefAny,
8    resources::OptionImageRef,
9};
10use azul_css::{
11    dynamic_selector::CssPropertyWithConditionsVec,
12    props::{
13        basic::*,
14        layout::*,
15        property::{CssProperty, *},
16        style::*,
17    },
18    *,
19};
20
21use crate::{
22    callbacks::{Callback, CallbackInfo},
23    widgets::button::{Button, ButtonOnClick, ButtonOnClickCallback},
24};
25
26#[derive(Debug, Clone, PartialEq)]
27#[repr(C)]
28pub 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
45impl 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)]
61pub 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
70impl 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)]
84pub struct FileInputState {
85    pub path: OptionString,
86}
87
88impl 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
95pub type FileInputOnPathChangeCallbackType =
96    extern "C" fn(RefAny, CallbackInfo, FileInputState) -> Update;
97
98impl_widget_callback!(
99    FileInputOnPathChange,
100    OptionFileInputOnPathChange,
101    FileInputOnPathChangeCallback,
102    FileInputOnPathChangeCallbackType
103);
104
105azul_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
118impl 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
203extern "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}