This commit is contained in:
Brandon Thomas 2026-10-09 20:36:08 -04:00
commit e87863d48d
3 changed files with 45 additions and 5 deletions

View File

@ -1670,7 +1670,8 @@ mod tests {
let parent_index = dir.to_string_lossy().split(['/', '\\']).filter(|p| !p.is_empty()).count() - 1;
assert_eq!(h.request("ui.clickWidget", json!({"id": format!("crumb:{parent_index}")}), t)["ok"], true);
h.settle(SETTLE);
assert_eq!(h.app.session.browse.as_ref().unwrap().path, dir.to_string_lossy().replace('\\', "/"));
// the same folder, whatever the platform's separators (issue #538)
assert_eq!(std::path::Path::new(&h.app.session.browse.as_ref().unwrap().path), dir.as_path());
let _ = std::fs::remove_dir_all(&dir);
}

View File

@ -717,10 +717,33 @@ fn folder_header(app: &mut LightcraftApp, ui: &mut egui::Ui, b: &lightcraft_engi
header.response.rect
}
/// The folders a breadcrumb shows for `path`: each name with the path that opens it, spelled as
/// in `path` (its separators, a leading `/` or a UNC `\\server\share`; issue #538). A bare drive
/// (`C:`) gets its separator back: on its own it means the current folder on that drive.
pub(crate) fn crumbs(path: &str) -> Vec<(&str, String)> {
let mut out = Vec::new();
let mut start = 0;
for (i, c) in path.char_indices().chain(std::iter::once((path.len(), '/'))) {
if c != '/' && c != '\\' {
continue;
}
if let Some(part) = path.get(start..i).filter(|p| !p.is_empty()) {
let mut to = path.get(..i).unwrap_or(path).to_string();
if out.is_empty() && part.ends_with(':') && to == part {
to.push(if path.contains('\\') { '\\' } else { '/' });
}
out.push((part, to));
}
start = i + c.len_utf8();
}
out
}
fn folder_breadcrumbs(app: &mut LightcraftApp, ui: &mut egui::Ui, path: &str) {
let t = Tokens::get(ui.ctx());
ui.spacing_mut().item_spacing.x = 4.0;
let parts: Vec<&str> = path.split(['/', '\\']).filter(|p| !p.is_empty()).collect();
let crumbs = crumbs(path);
let parts: Vec<&str> = crumbs.iter().map(|(name, _)| *name).collect();
// With all sidebars open even four ellipses and their separators can exceed the row.
// Prefer the current folder, showing more ancestors as space permits.
let visible = ((ui.available_width() / 80.0) as usize).clamp(1, 4);
@ -741,9 +764,10 @@ fn folder_breadcrumbs(app: &mut LightcraftApp, ui: &mut egui::Ui, path: &str) {
if r.hovered() {
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
}
if r.clicked() {
let prefix = if path.starts_with('/') { format!("/{}", parts[..=i].join("/")) } else { parts[..=i].join("/") };
let _ = app.run("library.browse", json!({"path": prefix}));
if r.clicked()
&& let Some((_, to)) = crumbs.get(i)
{
let _ = app.run("library.browse", json!({"path": to}));
}
ui.label(egui::RichText::new("›").size(13.0).color(t.text_dim));
}

View File

@ -2,6 +2,21 @@ use egui::{Rect, vec2};
use crate::{LightcraftApp, Services, headless::HeadlessView, i18n::Locale};
/// Issue #538: a breadcrumb opens its folder spelled as the path is, keeping a UNC share's leading
/// `\\` and giving a bare drive its root back.
#[test]
fn breadcrumbs_open_their_folder_as_spelled() {
let to = |path: &str| super::crumbs(path).into_iter().map(|(name, to)| (name.to_string(), to)).collect::<Vec<_>>();
let pairs = |v: &[(&str, &str)]| v.iter().map(|(a, b)| (a.to_string(), b.to_string())).collect::<Vec<_>>();
assert_eq!(to("/Users/me/Pictures"), pairs(&[("Users", "/Users"), ("me", "/Users/me"), ("Pictures", "/Users/me/Pictures")]));
assert_eq!(to(r"C:\Users\me"), pairs(&[("C:", r"C:\"), ("Users", r"C:\Users"), ("me", r"C:\Users\me")]));
assert_eq!(to("C:/Users"), pairs(&[("C:", "C:/"), ("Users", "C:/Users")]));
assert_eq!(to(r"\\server\share\photos"), pairs(&[("server", r"\\server"), ("share", r"\\server\share"), ("photos", r"\\server\share\photos")]));
assert_eq!(to("relative/dir/"), pairs(&[("relative", "relative"), ("dir", "relative/dir")]));
assert_eq!(to("/Fotos/día 1/写真"), pairs(&[("Fotos", "/Fotos"), ("día 1", "/Fotos/día 1"), ("写真", "/Fotos/día 1/写真")]));
assert!(to("").is_empty() && to("/").is_empty() && to(r"\\").is_empty());
}
fn text_bounds(shape: &egui::Shape, out: &mut Vec<Rect>) {
match shape {
egui::Shape::Text(text) => out.push(text.visual_bounding_rect()),