1use azul_core::{
5 callbacks::{CoreCallbackData, Update},
6 dom::Dom,
7 refany::RefAny,
8 resources::OptionImageRef,
9};
10#[allow(clippy::wildcard_imports)] use azul_css::{
12 dynamic_selector::CssPropertyWithConditionsVec,
13 props::{
14 basic::*,
15 layout::*,
16 property::{CssProperty, *},
17 style::*,
18 },
19 *,
20};
21
22use crate::{
23 callbacks::{Callback, CallbackInfo},
24 widgets::button::{Button, ButtonOnClick, ButtonOnClickCallback},
25};
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28#[repr(C)]
29pub struct FileInput {
30 pub file_input_state: FileInputStateWrapper,
32 pub default_text: AzString,
35
36 pub image: OptionImageRef,
38 pub container_style: CssPropertyWithConditionsVec,
40 pub label_style: CssPropertyWithConditionsVec,
42 pub image_style: CssPropertyWithConditionsVec,
44}
45
46impl Default for FileInput {
47 fn default() -> Self {
48 let default_button = Button::create(AzString::from_const_str(""));
49 Self {
50 file_input_state: FileInputStateWrapper::default(),
51 default_text: "Select File...".into(),
52 image: None.into(),
53 container_style: default_button.container_style,
54 label_style: default_button.label_style,
55 image_style: default_button.image_style,
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61#[repr(C)]
62pub struct FileInputStateWrapper {
63 pub inner: FileInputState,
64 pub on_path_change: OptionFileInputOnPathChange,
65 pub file_dialog_title: AzString,
67 pub default_dir: OptionString,
69}
70
71impl Default for FileInputStateWrapper {
72 fn default() -> Self {
73 Self {
74 inner: FileInputState::default(),
75 on_path_change: None.into(),
76 file_dialog_title: "Select File".into(),
77 default_dir: None.into(),
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84#[repr(C)]
85pub struct FileInputState {
86 pub path: OptionString,
87}
88
89impl Default for FileInputState {
90 fn default() -> Self {
91 Self { path: None.into() }
92 }
93}
94
95pub type FileInputOnPathChangeCallbackType =
97 extern "C" fn(RefAny, CallbackInfo, FileInputState) -> Update;
98
99impl_widget_callback!(
100 FileInputOnPathChange,
101 OptionFileInputOnPathChange,
102 FileInputOnPathChangeCallback,
103 FileInputOnPathChangeCallbackType
104);
105
106azul_core::impl_managed_callback! {
107 wrapper: FileInputOnPathChangeCallback,
108 info_ty: CallbackInfo,
109 return_ty: Update,
110 default_ret: Update::DoNothing,
111 invoker_static: FILE_INPUT_ON_PATH_CHANGE_INVOKER,
112 invoker_ty: AzFileInputOnPathChangeCallbackInvoker,
113 thunk_fn: az_file_input_on_path_change_callback_thunk,
114 setter_fn: AzApp_setFileInputOnPathChangeCallbackInvoker,
115 from_handle_fn: AzFileInputOnPathChangeCallback_createFromHostHandle,
116 extra_args: [ state: FileInputState ],
117}
118
119impl FileInput {
120 #[must_use] pub fn create(path: OptionString) -> Self {
121 Self {
122 file_input_state: FileInputStateWrapper {
123 inner: FileInputState { path },
124 ..Default::default()
125 },
126 ..Default::default()
127 }
128 }
129
130 #[inline]
131 #[must_use]
132 pub fn swap_with_default(&mut self) -> Self {
133 let mut s = Self::create(None.into());
134 core::mem::swap(&mut s, self);
135 s
136 }
137
138 #[inline]
139 pub fn set_default_text(&mut self, default_text: AzString) {
140 self.default_text = default_text;
141 }
142
143 #[inline]
144 #[must_use] pub fn with_default_text(mut self, default_text: AzString) -> Self {
145 self.set_default_text(default_text);
146 self
147 }
148
149 #[inline]
150 pub fn set_on_path_change<I: Into<FileInputOnPathChangeCallback>>(
151 &mut self,
152 refany: RefAny,
153 callback: I,
154 ) {
155 self.file_input_state.on_path_change = Some(FileInputOnPathChange {
156 callback: callback.into(),
157 refany,
158 })
159 .into();
160 }
161
162 #[inline]
163 #[must_use]
164 pub fn with_on_path_change<I: Into<FileInputOnPathChangeCallback>>(
165 mut self,
166 refany: RefAny,
167 callback: I,
168 ) -> Self {
169 self.set_on_path_change(refany, callback);
170 self
171 }
172
173 #[inline]
174 #[must_use] pub fn dom(self) -> Dom {
175 let button_label = match self.file_input_state.inner.path.as_ref() {
178 Some(path) => std::path::Path::new(path.as_str())
179 .file_name()
180 .map_or_else(
181 || self.default_text.as_str().to_string(),
182 |s| s.to_string_lossy().to_string(),
183 )
184 .into(),
185 None => self.default_text.clone(),
186 };
187
188 Button {
189 label: button_label,
190 image: self.image,
191 button_type: crate::widgets::button::ButtonType::Default,
192 container_style: self.container_style,
193 label_style: self.label_style,
194 image_style: self.image_style,
195 on_click: Some(ButtonOnClick {
196 refany: RefAny::new(self.file_input_state),
197 callback: ButtonOnClickCallback {
198 cb: fileinput_on_click,
199 ctx: azul_core::refany::OptionRefAny::None,
200 },
201 })
202 .into(),
203 }
204 .dom()
205 }
206}
207
208extern "C" fn fileinput_on_click(mut refany: RefAny, mut info: CallbackInfo) -> Update {
209 let Some(mut fileinputstatewrapper) = refany.downcast_mut::<FileInputStateWrapper>() else {
210 return Update::DoNothing;
211 };
212 let fileinputstatewrapper = &mut *fileinputstatewrapper;
213
214 #[cfg(all(feature = "extra", not(any(target_os = "android", target_os = "ios"))))]
218 {
219 let mut dialog = tfd::FileDialog::new(fileinputstatewrapper.file_dialog_title.as_str());
220 if let Some(dir) = fileinputstatewrapper.default_dir.as_ref() {
221 dialog = dialog.with_path(dir.as_str());
222 }
223 let Some(selected_path) = dialog.open_file() else {
224 return Update::DoNothing;
225 };
226 fileinputstatewrapper.inner.path = Some(selected_path.into()).into();
227 }
228
229 let inner = fileinputstatewrapper.inner.clone();
230 let mut result = match fileinputstatewrapper.on_path_change.as_mut() {
231 Some(FileInputOnPathChange { refany, callback }) => {
232 (callback.cb)(refany.clone(), info, inner)
233 }
234 None => Update::RefreshDom,
235 };
236
237 result.max_self(Update::RefreshDom);
238
239 result
240}