UI: the drop hint and drop errors go through the interface catalog; the Wayland notice points to the README (#447 follow-up)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
This commit is contained in:
Claude 2026-10-10 12:55:10 +00:00
parent 63acbb1eb0
commit 00e9e37c0a
No known key found for this signature in database
3 changed files with 39 additions and 101 deletions

View File

@ -52,7 +52,7 @@ fn is_project_file(path: &str) -> bool {
#[cfg(target_os = "linux")]
fn wayland_drop_notice(wayland_display: Option<&std::ffi::OsStr>, backend: Option<&std::ffi::OsStr>) -> Option<&'static str> {
(wayland_display.is_some_and(|v| !v.is_empty()) && backend != Some(std::ffi::OsStr::new("x11")))
.then_some("Native Wayland file drops are unavailable. Use File > Import > File... or run via XWayland (see docs/footage.md).")
.then_some("Native Wayland file drops are unavailable. Use File ▸ Import ▸ File… or run via XWayland (see README, Linux).")
}
/// The command line.
@ -196,10 +196,8 @@ fn main() -> eframe::Result {
app.offer_recovery(r);
}
#[cfg(target_os = "linux")]
if let Some(note) = wayland_drop_notice(
std::env::var_os("WAYLAND_DISPLAY").as_deref(),
std::env::var_os("WINIT_UNIX_BACKEND").as_deref(),
) && app.ui.status.is_empty()
if let Some(note) = wayland_drop_notice(std::env::var_os("WAYLAND_DISPLAY").as_deref(), std::env::var_os("WINIT_UNIX_BACKEND").as_deref())
&& app.ui.status.is_empty()
{
app.ui.status = note.into();
}
@ -386,7 +384,7 @@ mod tests {
fn wayland_drop_notice_recommends_import_or_xwayland() {
use std::ffi::OsStr;
let warning = super::wayland_drop_notice(Some(OsStr::new("wayland-0")), None).unwrap();
assert!(warning.contains("File > Import") && warning.contains("XWayland"));
assert!(warning.contains("File ▸ Import") && warning.contains("XWayland"));
assert!(super::wayland_drop_notice(None, None).is_none());
assert!(super::wayland_drop_notice(Some(OsStr::new("")), None).is_none());
assert!(super::wayland_drop_notice(Some(OsStr::new("wayland-0")), Some(OsStr::new("x11"))).is_none());

View File

@ -25,30 +25,13 @@ pub fn dropped_files(app: &mut EffectcraftApp, ui: &egui::Ui, bounds: Rect) {
let ctx = ui.ctx().clone();
let hovered = ctx.input(|i| i.raw.hovered_files.len());
if hovered != 0 {
let label = if hovered == 1 {
"Drop file to import".to_string()
} else {
format!("Drop {hovered} files to import")
};
let painter = ctx.layer_painter(egui::LayerId::new(
egui::Order::Foreground,
egui::Id::new("native-file-drop"),
));
painter.rect_stroke(
bounds.shrink(8.0),
6.0,
Stroke::new(2.0, app.tokens.accent),
StrokeKind::Inside,
);
let label =
if hovered == 1 { crate::i18n::tr("Drop file to import").to_string() } else { crate::i18n::tr_args("Drop {} files to import", &[&hovered]) };
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, egui::Id::new("native-file-drop")));
painter.rect_stroke(bounds.shrink(8.0), 6.0, Stroke::new(2.0, app.tokens.accent), StrokeKind::Inside);
let hint = Rect::from_center_size(bounds.center(), vec2(300.0, 46.0));
painter.rect_filled(hint, 6.0, app.tokens.panel_bg);
painter.text(
hint.center(),
Align2::CENTER_CENTER,
&label,
crate::theme::Tokens::medium(14.0),
app.tokens.text,
);
painter.text(hint.center(), Align2::CENTER_CENTER, &label, crate::theme::Tokens::medium(14.0), app.tokens.text);
app.auto.add("file.dropTarget", hint, &label);
}
@ -71,9 +54,7 @@ pub fn dropped_files(app: &mut EffectcraftApp, ui: &egui::Ui, bounds: Rect) {
let mut errors = Vec::new();
if skipped != 0 {
errors.push(format!(
"Skipped {skipped} dropped file(s) without a local path. Use File > Import > File..."
));
errors.push(crate::i18n::tr_args("Skipped {} dropped file(s) without a local path. Use File ▸ Import ▸ File…", &[&skipped]));
}
let (proj, rest): (Vec<String>, Vec<String>) = paths.into_iter().partition(|p| {
@ -82,30 +63,25 @@ pub fn dropped_files(app: &mut EffectcraftApp, ui: &egui::Ui, bounds: Rect) {
});
if proj.len() > 1 {
errors.push(format!(
"Only one project can be opened per drop; skipped {} more",
proj.len() - 1
));
errors.push(crate::i18n::tr_args("Only one project can be opened per drop; skipped {} more", &[&proj.len().saturating_sub(1)]));
}
if let Some(p) = proj.first()
&& let Err(e) = crate::menus::invoke(app, &ctx, "file.open", json!({"path": p}))
{
errors.push(format!("Cannot open dropped project {p}: {e}"));
errors.push(crate::i18n::tr_args("Cannot open dropped project {}: {}", &[&p, &e]));
}
if !rest.is_empty() {
let viewer = app.auto.elements.iter().find(|e| e.id == "viewer.area").map(|e| {
Rect::from_min_size(
egui::pos2(e.rect[0], e.rect[1]),
egui::vec2(e.rect[2], e.rect[3]),
)
});
let viewer = app
.auto
.elements
.iter()
.find(|e| e.id == "viewer.area")
.map(|e| Rect::from_min_size(egui::pos2(e.rect[0], e.rect[1]), egui::vec2(e.rect[2], e.rect[3])));
let at = if proj.is_empty() {
ctx.input(|i| i.pointer.hover_pos())
.filter(|p| viewer.is_some_and(|v| v.contains(*p)))
.and_then(|p| crate::panels::viewer::screen_to_comp(&ctx, p))
ctx.input(|i| i.pointer.hover_pos()).filter(|p| viewer.is_some_and(|v| v.contains(*p))).and_then(|p| crate::panels::viewer::screen_to_comp(&ctx, p))
} else {
None
};
@ -120,7 +96,7 @@ pub fn dropped_files(app: &mut EffectcraftApp, ui: &egui::Ui, bounds: Rect) {
});
if let Err(e) = crate::menus::invoke(app, &ctx, "file.import", params) {
errors.push(format!("Cannot import dropped files: {e}"));
errors.push(crate::i18n::tr_args("Cannot import dropped files: {}", &[&e]));
}
}
@ -191,21 +167,10 @@ fn video_preview(app: &mut EffectcraftApp, ctx: &egui::Context) {
let opts = crate::panels::viewer::zoom_texture_options(app.session.prefs.viewer_zoom_smooth());
let t = match prev {
Some((_, mut t)) => {
t.set(
crate::frames::fit_texture(
(*img).clone(),
crate::frames::max_texture_side(ctx),
),
opts,
);
t.set(crate::frames::fit_texture((*img).clone(), crate::frames::max_texture_side(ctx)), opts);
t
}
None => crate::frames::load_fitted(
ctx,
"video-preview",
(*img).clone(),
opts,
),
None => crate::frames::load_fitted(ctx, "video-preview", (*img).clone(), opts),
};
ctx.data_mut(|d| d.insert_temp(tex_id, (k, t.clone())));
Some(t)
@ -215,55 +180,30 @@ fn video_preview(app: &mut EffectcraftApp, ctx: &egui::Context) {
(_, _, prev) => prev.map(|p| p.1),
};
let title = format!(
"Video Preview — {}",
app.session
.active_comp_id()
.and_then(|c| app.session.project.item(c))
.map(|i| i.name.clone())
.unwrap_or_default()
);
let title =
format!("Video Preview — {}", app.session.active_comp_id().and_then(|c| app.session.project.item(c)).map(|i| i.name.clone()).unwrap_or_default());
let mut builder = egui::ViewportBuilder::default()
.with_title(title)
.with_inner_size([960.0, 540.0]);
let mut builder = egui::ViewportBuilder::default().with_title(title).with_inner_size([960.0, 540.0]);
if v.device == "fullscreen" {
builder = builder.with_fullscreen(true);
}
let mut closed = false;
ctx.show_viewport_immediate(
egui::ViewportId::from_hash_of("effectcraft-video-preview"),
builder,
|vctx, _| {
egui::CentralPanel::default()
.frame(egui::Frame::NONE.fill(Color32::BLACK))
.show(vctx, |ui| {
let area = ui.max_rect();
if let Some(t) = &tex {
let [w, h] = t.size().map(|x| x.max(1) as f32);
let s = (area.width() / w).min(area.height() / h);
let r = Rect::from_center_size(
area.center(),
egui::vec2(w * s, h * s),
);
ui.painter().image(
t.id(),
r,
Rect::from_min_max(
egui::pos2(0.0, 0.0),
egui::pos2(1.0, 1.0),
),
Color32::WHITE,
);
}
});
if vctx.input(|i| i.viewport().close_requested()) {
closed = true;
ctx.show_viewport_immediate(egui::ViewportId::from_hash_of("effectcraft-video-preview"), builder, |vctx, _| {
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(Color32::BLACK)).show(vctx, |ui| {
let area = ui.max_rect();
if let Some(t) = &tex {
let [w, h] = t.size().map(|x| x.max(1) as f32);
let s = (area.width() / w).min(area.height() / h);
let r = Rect::from_center_size(area.center(), egui::vec2(w * s, h * s));
ui.painter().image(t.id(), r, Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), Color32::WHITE);
}
},
);
});
if vctx.input(|i| i.viewport().close_requested()) {
closed = true;
}
});
if closed && let Err(e) = app.set_pref("video.enableOutput", Value::Bool(false)) {
app.ui.status = e;

View File

@ -277,7 +277,7 @@ fn dropped_file_without_local_path_explains_the_fallback() {
h.input_mut().dropped_files.push(Arc::new(Dropped("relative.mp4".into())));
h.run_steps(2);
assert!(h.state().ui.status.contains("without a local path"), "{}", h.state().ui.status);
assert!(h.state().ui.status.contains("File > Import"));
assert!(h.state().ui.status.contains("File ▸ Import"));
}
#[test]