Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

File Dialog

Group: Overlays

Code

/// Materialise a small, fixed directory tree for the native dialogs to
/// list — see `file_picker.rs`'s `build_demo_tree` for why this is a
/// synthetic tempdir rather than this crate's own source tree or
/// `CARGO_MANIFEST_DIR`.
fn build_demo_tree() -> TempDir {
    let dir = tempfile::tempdir().expect("create a temp dir for the file dialog demo");
    let root = dir.path();
    std::fs::write(root.join("README.md"), b"# quadraui gallery demo tree\n")
        .expect("write README.md");
    std::fs::write(root.join("main.rs"), b"fn main() {}\n").expect("write main.rs");
    dir
}

pub struct FileDialogDemo {
    root_dir: TempDir,
    status: String,
    picked: Option<PathBuf>,
}

impl FileDialogDemo {
    pub fn new() -> Self {
        Self {
            root_dir: build_demo_tree(),
            status: "Enter (or o/s/f) opens the dialog".to_string(),
            picked: None,
        }
    }

    fn variant_label(variant: usize) -> &'static str {
        match variant {
            0 => "Open",
            1 => "Save as",
            _ => "Folder",
        }
    }

    fn base_opts(&self) -> FileDialogOptions {
        FileDialogOptions {
            initial_dir: Some(self.root_dir.path().to_path_buf()),
            ..Default::default()
        }
    }

    /// Record a dialog outcome. `path` is `None` when the user cancelled
    /// (or the backend can't offer that dialog at all).
    fn record(&mut self, ok_prefix: &str, cancel_label: &str, path: Option<PathBuf>) {
        match path {
            Some(p) => {
                self.status = format!("{ok_prefix}: {}", p.display());
                self.picked = Some(p);
            }
            None => {
                self.status = format!("{cancel_label} cancelled (or unsupported on this backend)");
                self.picked = None;
            }
        }
    }

    /// The outcome message goes in a **left** segment and only the
    /// picked path's final component goes in the right one — a right
    /// segment carrying a full absolute path clips at the bar's right
    /// edge and can blank the whole message; see `file_picker.rs`'s
    /// sibling `FileDialogDemo::status_bar` in `quadraui/examples` for
    /// the regression this split avoids.
    fn status_bar(&self, variant: usize) -> StatusBar {
        StatusBar {
            id: WidgetId::new("gallery:file-dialog:status"),
            left_segments: vec![StatusBarSegment {
                text: format!(" [{}] {} ", Self::variant_label(variant), self.status),
                fg: Color::rgb(220, 220, 220),
                bg: Color::rgb(40, 80, 120),
                bold: false,
                action_id: None,
            }],
            right_segments: match &self.picked {
                Some(p) => {
                    let leaf = p
                        .file_name()
                        .map(|n| n.to_string_lossy().into_owned())
                        .unwrap_or_else(|| p.display().to_string());
                    vec![StatusBarSegment {
                        text: format!(" \u{2713} {leaf} "),
                        fg: Color::rgb(150, 240, 150),
                        bg: Color::rgb(30, 80, 30),
                        bold: false,
                        action_id: None,
                    }]
                }
                None => vec![],
            },
        }
    }

    fn open_file(&mut self, backend: &mut dyn Backend) {
        let opts = FileDialogOptions {
            title: Some("Open File".to_string()),
            filters: vec![("Rust files".to_string(), vec!["rs".to_string()])],
            ..self.base_opts()
        };
        let picked = backend.services().show_file_open_dialog(opts);
        self.record("Opened", "Open", picked);
    }

    fn save_file(&mut self, backend: &mut dyn Backend) {
        let opts = FileDialogOptions {
            title: Some("Save As".to_string()),
            initial_filename: Some("untitled.txt".to_string()),
            ..self.base_opts()
        };
        let picked = backend.services().show_file_save_dialog(opts);
        self.record("Save as", "Save", picked);
    }

    fn open_folder(&mut self, backend: &mut dyn Backend) {
        let opts = FileDialogOptions {
            title: Some("Open Folder".to_string()),
            ..self.base_opts()
        };
        let picked = backend.services().show_folder_open_dialog(opts);
        self.record("Folder", "Folder pick", picked);
    }
}

impl Default for FileDialogDemo {
    fn default() -> Self {
        Self::new()
    }
}

impl Demo for FileDialogDemo {
    fn name(&self) -> &'static str {
        "File Dialog"
    }

    fn group(&self) -> &'static str {
        "Overlays"
    }

    fn variants(&self) -> &'static [&'static str] {
        &["Open", "Save as", "Folder"]
    }

    fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
        let lh = backend.line_height();
        let status_rect = Rect::new(area.x, area.y + area.height - lh, area.width, lh);
        let _ = backend.draw_status_bar_interactive(
            status_rect,
            &self.status_bar(variant),
            &InteractionState::new(),
        );
    }

    fn handle(
        &mut self,
        variant: usize,
        event: &UiEvent,
        backend: &mut dyn Backend,
        _area: Rect,
    ) -> Reaction {
        match event {
            UiEvent::KeyPressed {
                key: Key::Char('o'),
                ..
            } => {
                self.open_file(backend);
                Reaction::Redraw
            }
            UiEvent::KeyPressed {
                key: Key::Char('s'),
                ..
            } => {
                self.save_file(backend);
                Reaction::Redraw
            }
            UiEvent::KeyPressed {
                key: Key::Char('f'),
                ..
            } => {
                self.open_folder(backend);
                Reaction::Redraw
            }
            UiEvent::KeyPressed {
                key: Key::Named(quadraui::NamedKey::Enter),
                ..
            } => {
                match variant {
                    0 => self.open_file(backend),
                    1 => self.save_file(backend),
                    _ => self.open_folder(backend),
                }
                Reaction::Redraw
            }
            _ => Reaction::Continue,
        }
    }

    fn source(&self) -> &'static str {
        extract_region(SOURCE)
    }

    fn data(&self, variant: usize) -> serde_json::Value {
        serde_json::json!({
            "variant": Self::variant_label(variant),
            "root": self.root_dir.path().display().to_string(),
            "status": self.status,
            "picked": self.picked.as_ref().map(|p| p.display().to_string()),
        })
    }

    fn caps_note(&self, variant: usize, caps: &BackendCaps) -> Option<String> {
        match variant {
            0 | 1 if !caps.file_dialogs => Some(
                "This backend has no native file-picker facility (BackendCaps::file_dialogs is \
                 false) — open/save still resolve a real path through an in-canvas \
                 FilePickerController driven by a nested draw-and-read loop."
                    .into(),
            ),
            2 if !caps.folder_dialogs => Some(
                "This backend has no native folder-picker facility (BackendCaps::folder_dialogs \
                 is false) and no in-canvas fallback is wired up for it yet — the folder dialog \
                 degrades all the way to None."
                    .into(),
            ),
            _ => None,
        }
    }
}

Backends

VariantTUIGTKmacOSWindows
Open
Save as
Folder