This commit is contained in:
Claude
2026-10-08 17:18:57 +00:00
3 changed files with 56 additions and 2 deletions
+2
View File
@@ -29,6 +29,8 @@ pub mod widgets;
#[cfg(test)]
mod tests_curve;
#[cfg(test)]
mod tests_filmstrip;
#[cfg(test)]
mod tests_grid;
#[cfg(test)]
mod tests_library_problem;
+24 -2
View File
@@ -1493,6 +1493,12 @@ fn eye_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Response
// ------------------------------------------------------------------------ filmstrip
/// A filmstrip cell's file-name label: names longer than 14 characters are cut to 13 and an
/// ellipsis. Counts characters, not bytes, so a CJK name is never cut inside a character (#266).
fn film_label(name: &str) -> String {
if name.chars().nth(14).is_some() { format!("{}…", name.chars().take(13).collect::<String>()) } else { name.to_string() }
}
pub(crate) fn filmstrip(app: &mut LightcraftApp, ui: &mut egui::Ui, r: Rect) {
let t = Tokens::get(ui.ctx());
ui.painter().rect_filled(r, 0.0, t.canvas);
@@ -1536,8 +1542,7 @@ pub(crate) fn filmstrip(app: &mut LightcraftApp, ui: &mut egui::Ui, r: Rect) {
let names = app.ui.settings.film_names;
if let Some(ph) = app.session.catalog.photo(*id).filter(|_| names) {
let name = ph.file_name.rsplit_once('.').map(|(n, _)| n).unwrap_or(&ph.file_name);
let short: String = if name.len() > 14 { format!("{}…", &name[..13]) } else { name.to_string() };
p.text(pos2(cr.left() + 8.0, cr.top() + 10.0), Align2::LEFT_CENTER, short, t.font(10.0), t.text_dim);
p.text(pos2(cr.left() + 8.0, cr.top() + 10.0), Align2::LEFT_CENTER, film_label(name), t.font(10.0), t.text_dim);
p.text(pos2(cr.right() - 8.0, cr.top() + 10.0), Align2::RIGHT_CENTER, &ph.format, t.semibold(8.5), t.text_dim);
}
let img_area = Rect::from_min_max(cr.min + vec2(10.0, 22.0), cr.max - vec2(10.0, 8.0));
@@ -1648,3 +1653,20 @@ fn straighten_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::R
}
}
}
#[cfg(test)]
mod tests {
use super::film_label;
#[test]
fn film_labels_cut_on_characters_not_bytes() {
// the name from #266: byte 13 falls inside '限'
assert_eq!(film_label("202407層三限定訂閱圖(4)"), "202407層三限定訂閱圖…");
assert_eq!(film_label("写真"), "写真");
// exactly 14 characters (42 bytes) is shown whole
assert_eq!(film_label("一二三四五六七八九十一二三四"), "一二三四五六七八九十一二三四");
assert_eq!(film_label("IMG_20240712_153012"), "IMG_20240712_…");
assert_eq!(film_label("DSC_0001"), "DSC_0001");
assert_eq!(film_label(""), "");
}
}
+30
View File
@@ -0,0 +1,30 @@
//! Headless test of the filmstrip's file-name labels (#266): a name with multi-byte (CJK)
//! characters is drawn without panicking.
use std::time::Duration;
use lightcraft_catalog::Op;
use serde_json::json;
use crate::headless::Headless;
use crate::{LightcraftApp, Services};
const T: Duration = Duration::from_secs(20);
const SETTLE: Duration = Duration::from_secs(120);
#[test]
fn filmstrip_shows_a_cjk_file_name() {
let mut session = lightcraft_engine::Session::with_demo();
let first = session.visible_cloned()[0];
let source = session.catalog.photo(first).expect("demo photo").source.clone();
// the name from #266: cutting it at byte 13 lands inside '限'
session.catalog.apply(Op::SetFile { id: first, file_name: "202407層三限定訂閱圖(4).jpg".into(), source }).unwrap();
let app = LightcraftApp::new(session, Services { png: None, ..Default::default() });
let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
let r = h.request("ui.set", json!({"view": "detail"}), T);
assert_eq!(r["ok"], true, "{r}");
h.settle(SETTLE);
assert!(h.app.ui.settings.film_names);
let film = format!("film:{}", first.0);
assert!(h.app.widgets.iter().any(|(w, _)| *w == film), "{film} drawn in the filmstrip");
}