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 Picker

Group: Content

Code

/// Materialise a small, fixed directory tree for the pickers to list.
///
/// Deliberately **not** this crate's own `src/demos/` (that directory's
/// contents change every time a future demo is ported, so the picker's
/// listing — and any capture-mode snapshot of it — would silently churn
/// on unrelated PRs) and **not** a path derived from
/// `env!("CARGO_MANIFEST_DIR")` (a build-machine path baked in at
/// compile time; `CLAUDE.md`'s `cargo xwin test` flow runs the compiled
/// `.exe` on a separate Windows host where that path doesn't resolve).
///
/// The returned [`TempDir`]'s own path is unique per run, but the file
/// *names* inside it are fixed, so the listing itself is deterministic.
/// Callers must keep the `TempDir` alive for as long as the picker
/// needs its contents — dropping it deletes the directory.
fn build_demo_tree() -> TempDir {
    let dir = tempfile::tempdir().expect("create a temp dir for the file picker 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");
    std::fs::create_dir(root.join("src")).expect("create src/");
    std::fs::write(root.join("src").join("lib.rs"), b"// lib\n").expect("write src/lib.rs");
    std::fs::create_dir(root.join("assets")).expect("create assets/");
    std::fs::write(root.join("assets").join("logo.svg"), b"<svg/>\n")
        .expect("write assets/logo.svg");
    dir
}

pub struct FilePickerDemo {
    // Kept alive for `FilePickerDemo`'s whole lifetime: the pickers
    // below hold only the `PathBuf` this resolves to, not the `TempDir`
    // itself, and dropping it would delete the tree out from under them.
    root_dir: TempDir,
    open_picker: FilePickerController,
    save_picker: FilePickerController,
    folder_picker: FolderPickerController,
    confirmed: Option<String>,
}

impl FilePickerDemo {
    pub fn new() -> Self {
        let root_dir = build_demo_tree();
        let root: PathBuf = root_dir.path().to_path_buf();
        Self {
            root_dir,
            open_picker: FilePickerController::new(FilePickerMode::Open, root.clone(), vec![]),
            save_picker: FilePickerController::new(FilePickerMode::Save, root.clone(), vec![])
                .with_initial_filename("untitled.rs"),
            folder_picker: FolderPickerController::new(root, vec![], false),
            confirmed: None,
        }
    }

    fn popup_rect(area: Rect, backend: &dyn Backend) -> Rect {
        let w = (area.width * 0.8).max(40.0);
        let h = (area.height * 0.8).max(12.0 * backend.line_height());
        let x = area.x + (area.width - w) / 2.0;
        let y = area.y + (area.height - h) / 2.0;
        Rect::new(x, y, w, h)
    }

    fn status_bar(&self) -> StatusBar {
        StatusBar {
            id: WidgetId::new("gallery:file-picker:status"),
            left_segments: vec![StatusBarSegment {
                text: format!(
                    " {} ",
                    self.confirmed
                        .as_deref()
                        .unwrap_or("navigate, Enter to confirm")
                ),
                fg: Color::rgb(220, 220, 220),
                bg: Color::rgb(40, 60, 100),
                bold: false,
                action_id: None,
            }],
            right_segments: vec![],
        }
    }

    fn visible_rows(popup_rect: Rect, backend: &dyn Backend) -> usize {
        let lh = backend.line_height();
        let rows = if lh > 0.0 {
            (popup_rect.height / lh) as usize
        } else {
            24
        };
        rows.saturating_sub(PALETTE_CHROME_ROWS)
    }
}

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

impl Demo for FilePickerDemo {
    fn name(&self) -> &'static str {
        // Not "File & Folder Picker": the sidebar's demo-name column is
        // narrow enough (~20 cells) that the longer title clips, hiding
        // the final letter rather than revealing the folder variant.
        // The "Choose folder" entry in `variants()` is what actually
        // surfaces it, once this row is selected.
        "File Picker"
    }

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

    fn variants(&self) -> &'static [&'static str] {
        &["Open file", "Save file", "Choose folder"]
    }

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

        let popup_rect = Self::popup_rect(area, backend);
        match variant {
            0 => self.open_picker.render(popup_rect, backend),
            1 => self.save_picker.render(popup_rect, backend),
            _ => self.folder_picker.render(popup_rect, backend),
        }
    }

    fn handle(
        &mut self,
        variant: usize,
        event: &UiEvent,
        backend: &mut dyn Backend,
        area: Rect,
    ) -> Reaction {
        let popup_rect = Self::popup_rect(area, backend);
        let visible_rows = Self::visible_rows(popup_rect, backend);
        match variant {
            0 => match self.open_picker.handle(event, visible_rows) {
                FilePickerEvent::Confirmed { path } => {
                    self.confirmed = Some(format!("Opened: {}", path.display()));
                    Reaction::Redraw
                }
                FilePickerEvent::Cancelled => {
                    self.confirmed = Some("Dismissed".into());
                    Reaction::Redraw
                }
                FilePickerEvent::Consumed => Reaction::Redraw,
                FilePickerEvent::Ignored => Reaction::Continue,
            },
            1 => match self.save_picker.handle(event, visible_rows) {
                FilePickerEvent::Confirmed { path } => {
                    self.confirmed = Some(format!("Saved: {}", path.display()));
                    Reaction::Redraw
                }
                FilePickerEvent::Cancelled => {
                    self.confirmed = Some("Dismissed".into());
                    Reaction::Redraw
                }
                FilePickerEvent::Consumed => Reaction::Redraw,
                FilePickerEvent::Ignored => Reaction::Continue,
            },
            _ => match self.folder_picker.handle(event, visible_rows) {
                FolderPickerEvent::Confirmed { path } => {
                    self.confirmed = Some(format!("Chose folder: {}", path.display()));
                    Reaction::Redraw
                }
                FolderPickerEvent::Cancelled => {
                    self.confirmed = Some("Dismissed".into());
                    Reaction::Redraw
                }
                FolderPickerEvent::Consumed => Reaction::Redraw,
                FolderPickerEvent::Ignored => Reaction::Continue,
            },
        }
    }

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

    fn data(&self, variant: usize) -> serde_json::Value {
        let mode = match variant {
            0 => "open",
            1 => "save",
            _ => "folder",
        };
        serde_json::json!({
            "mode": mode,
            "root": self.root_dir.path().display().to_string(),
            "confirmed": self.confirmed,
        })
    }

    fn caps_note(&self, _variant: usize, _caps: &BackendCaps) -> Option<String> {
        // Both controllers are in-app pickers (quadraui's own
        // palette-rendered list, not a native OS dialog), so they work
        // identically everywhere — there's no `BackendCaps` flag to
        // check and no gap to report.
        None
    }
}

Backends

VariantTUIGTKmacOSWindows
Open file
Save file
Choose folder