Skip to main content

kui_ffi/
dialogs.rs

1//! File dialogs as an ask: a view or a host asks for the
2//! platform's Open, Save or folder dialog, and the answer is a `files`
3//! event. Under `kui_run` the runner shows it; a host driving its own
4//! window drains the ask with `kui_take_file_request` and answers with
5//! `kui_input_files`.
6
7use super::*;
8
9/// `KUI_FILE_DIALOG_OPEN`: an existing file, or several.
10pub const KUI_FILE_DIALOG_OPEN: u32 = 0;
11/// `KUI_FILE_DIALOG_SAVE`: a path to write.
12pub const KUI_FILE_DIALOG_SAVE: u32 = 1;
13/// `KUI_FILE_DIALOG_FOLDER`: a folder, or several.
14pub const KUI_FILE_DIALOG_FOLDER: u32 = 2;
15
16/// One entry of a dialog's file-type menu.
17#[repr(C)]
18#[derive(Clone, Copy)]
19pub struct KuiFileFilter {
20    pub name: KuiStr,
21    /// `extension_count` extensions, without the dot.
22    pub extensions: *const KuiStr,
23    pub extension_count: usize,
24}
25
26/// The dialog [`kui_request_files`] asks for. Zeroed, it is an Open
27/// dialog for one file of any type.
28#[repr(C)]
29#[derive(Clone, Copy)]
30pub struct KuiFileDialog {
31    /// `KUI_FILE_DIALOG_*`.
32    pub mode: u32,
33    /// Nonzero: more than one file or folder may be picked.
34    pub multiple: u32,
35    /// Empty for the platform's own.
36    pub title: KuiStr,
37    pub filters: *const KuiFileFilter,
38    pub filter_count: usize,
39    /// The folder it opens in; empty for the platform's choice.
40    pub directory: KuiStr,
41    /// A save dialog's suggested name; empty for none.
42    pub file_name: KuiStr,
43}
44
45fn opt(s: KuiStr) -> Option<String> {
46    let s = kstr(s);
47    (!s.is_empty()).then(|| s.into_owned())
48}
49
50fn dialog_of(d: &KuiFileDialog) -> Result<kui_core::FileDialog, String> {
51    use kui_core::FileDialogMode;
52    let mode = match d.mode {
53        KUI_FILE_DIALOG_OPEN => FileDialogMode::Open,
54        KUI_FILE_DIALOG_SAVE => FileDialogMode::Save,
55        KUI_FILE_DIALOG_FOLDER => FileDialogMode::Folder,
56        m => {
57            return Err(format!(
58                "KuiFileDialog.mode must be a KUI_FILE_DIALOG_*, not {m}"
59            ));
60        }
61    };
62    let filters = if d.filters.is_null() || d.filter_count == 0 {
63        &[][..]
64    } else {
65        // SAFETY: the host promises `filter_count` filters at `filters`.
66        unsafe { std::slice::from_raw_parts(d.filters, d.filter_count) }
67    };
68    let filters = filters
69        .iter()
70        .map(|f| {
71            let exts = if f.extensions.is_null() || f.extension_count == 0 {
72                &[][..]
73            } else {
74                // SAFETY: as above, for the filter's own array.
75                unsafe { std::slice::from_raw_parts(f.extensions, f.extension_count) }
76            };
77            kui_core::FileFilter {
78                name: kstr(f.name).into_owned(),
79                extensions: exts
80                    .iter()
81                    .map(|e| kstr(*e).trim_start_matches('.').to_string())
82                    .collect(),
83            }
84        })
85        .collect();
86    Ok(kui_core::FileDialog {
87        mode,
88        multiple: d.multiple != 0,
89        title: opt(d.title),
90        filters,
91        directory: opt(d.directory),
92        file_name: opt(d.file_name),
93        tag: kui_core::Value::Null,
94    })
95}
96
97/// Asks for the platform's Open, Save or folder dialog. The answer is a
98/// `{kind:"files", paths, tag}` event — `paths` empty when the user
99/// cancelled — to whoever asked. `dialog` NULL is an Open dialog for one
100/// file; `tag` may be NULL and is consumed. False when one is already out
101/// (one dialog at a time) or the dialog is malformed.
102#[unsafe(no_mangle)]
103pub extern "C" fn kui_request_files(
104    ptr: *mut KuiCtx,
105    dialog: *const KuiFileDialog,
106    tag: *mut KuiValue,
107) -> bool {
108    guard(false, || {
109        let tag = take_msg(tag).unwrap_or(kui_core::Value::Null);
110        let Some(c) = (unsafe { ctx(ptr) }) else {
111            return false;
112        };
113        let mut d = match unsafe { dialog.as_ref() } {
114            None => kui_core::FileDialog::open(),
115            Some(d) => match dialog_of(d) {
116                Ok(d) => d,
117                Err(why) => {
118                    eprintln!("kui: kui_request_files: {why}");
119                    return false;
120                }
121            },
122        };
123        d.tag = tag;
124        c.core().request_files(d)
125    })
126}
127
128/// Whether a file dialog asked for is still unanswered.
129#[unsafe(no_mangle)]
130pub extern "C" fn kui_awaiting_files(ptr: *mut KuiCtx) -> bool {
131    guard(false, || {
132        unsafe { ctx(ptr) }.is_some_and(|c| c.core().awaiting_files())
133    })
134}
135
136fn lend(out: *mut KuiStr, s: &str) {
137    if let Some(o) = unsafe { out.as_mut() } {
138        *o = KuiStr {
139            ptr: s.as_ptr(),
140            len: s.len(),
141        };
142    }
143}
144
145/// Drains the dialog asked for, for a host that shows it itself (under
146/// `kui_run` the runner does): its `KUI_FILE_DIALOG_*` mode, whether it
147/// picks several, its title, folder and suggested name (empty for none),
148/// and how many filters it offers — read each with
149/// `kui_file_request_filter`. Any out pointer may be NULL; the strings are
150/// borrowed until the next call on this context. False when nothing is
151/// asked. Answer with `kui_input_files`.
152#[unsafe(no_mangle)]
153pub extern "C" fn kui_take_file_request(
154    ptr: *mut KuiCtx,
155    mode: *mut u32,
156    multiple: *mut bool,
157    title: *mut KuiStr,
158    directory: *mut KuiStr,
159    file_name: *mut KuiStr,
160    filter_count: *mut usize,
161) -> bool {
162    guard(false, || {
163        let Some(c) = (unsafe { ctx(ptr) }) else {
164            return false;
165        };
166        let Some(d) = c.core().take_file_requests().into_iter().next() else {
167            return false;
168        };
169        let code = match d.mode {
170            kui_core::FileDialogMode::Open => KUI_FILE_DIALOG_OPEN,
171            kui_core::FileDialogMode::Save => KUI_FILE_DIALOG_SAVE,
172            kui_core::FileDialogMode::Folder => KUI_FILE_DIALOG_FOLDER,
173        };
174        unsafe {
175            if let Some(m) = mode.as_mut() {
176                *m = code;
177            }
178            if let Some(m) = multiple.as_mut() {
179                *m = d.multiple;
180            }
181            if let Some(n) = filter_count.as_mut() {
182                *n = d.filters.len();
183            }
184        }
185        let d = c.file_request.insert(d);
186        lend(title, d.title.as_deref().unwrap_or(""));
187        lend(directory, d.directory.as_deref().unwrap_or(""));
188        lend(file_name, d.file_name.as_deref().unwrap_or(""));
189        true
190    })
191}
192
193/// Filter `i` of the dialog `kui_take_file_request` last handed out: its
194/// name, and its extensions joined with `;` (`"png;jpg"`, no dots).
195/// Borrowed until the next call; false for a filter that is not there.
196#[unsafe(no_mangle)]
197pub extern "C" fn kui_file_request_filter(
198    ptr: *mut KuiCtx,
199    i: usize,
200    name: *mut KuiStr,
201    extensions: *mut KuiStr,
202) -> bool {
203    guard(false, || {
204        let Some(c) = (unsafe { ctx(ptr) }) else {
205            return false;
206        };
207        let Some(f) = c.file_request.as_ref().and_then(|d| d.filters.get(i)) else {
208            return false;
209        };
210        c.file_filter_text = f.extensions.join(";");
211        let f = &c.file_request.as_ref().expect("checked above").filters[i];
212        lend(name, &f.name);
213        lend(extensions, &c.file_filter_text);
214        true
215    })
216}
217
218/// A file dialog's answer: the `count` paths picked, none for a cancelled
219/// dialog. Whoever asked hears `{kind:"files", paths, tag}`; with nothing
220/// asked it is dropped.
221#[unsafe(no_mangle)]
222pub extern "C" fn kui_input_files(ptr: *mut KuiCtx, paths: *const KuiStr, count: usize) {
223    guard((), || {
224        let paths = if paths.is_null() || count == 0 {
225            Vec::new()
226        } else {
227            // SAFETY: the host promises `count` strings at `paths`.
228            unsafe { std::slice::from_raw_parts(paths, count) }
229                .iter()
230                .map(|s| kstr(*s).into_owned())
231                .collect()
232        };
233        push_input(ptr, kui_core::InputEvent::Files(paths));
234    });
235}