mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
ui: import native file drops with drop-target hint (prefs_live)
This commit is contained in:
1 parent
53d6b396ac
commit
bf4d616fc5
1 file changed
+160
-38
@@ -11,41 +11,122 @@ use crate::frames::FrameKey;
|
||||
|
||||
/// Run once per frame, before the panels draw.
|
||||
pub fn frame(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
dropped_files(app, ctx);
|
||||
memory_watch(app, ctx);
|
||||
home_on_open(app, ctx);
|
||||
video_preview(app, ctx);
|
||||
}
|
||||
|
||||
/// Files dropped on the window are imported (layered files as Default Drag Import As says) in
|
||||
/// the background, with the Importing card showing what is read (#270). Dropped on the
|
||||
/// Composition viewer, they also become layers centred there, as in After Effects (#85), when the
|
||||
/// platform reports where the pointer is during file drags.
|
||||
fn dropped_files(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
let paths: Vec<String> =
|
||||
ctx.input(|i| i.raw.dropped_files.iter().map(|f| f.path().to_string_lossy().to_string()).filter(|p| std::path::Path::new(p).is_absolute()).collect());
|
||||
if paths.is_empty() {
|
||||
/// Import native file drops through the same command as the file picker. Run after the dock
|
||||
/// layout to use the current viewer rectangle for drops that also add layers.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn dropped_files(app: &mut EffectcraftApp, ui: &egui::Ui, bounds: Rect) {
|
||||
use egui::{Align2, Stroke, StrokeKind, vec2};
|
||||
|
||||
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 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,
|
||||
);
|
||||
app.auto.add("file.dropTarget", hint, &label);
|
||||
}
|
||||
|
||||
let (paths, skipped) = ctx.input(|i| {
|
||||
let mut paths = Vec::new();
|
||||
let mut skipped = 0;
|
||||
for file in &i.raw.dropped_files {
|
||||
if file.path().is_absolute() {
|
||||
paths.push(file.path().to_string_lossy().into_owned());
|
||||
} else {
|
||||
skipped += 1;
|
||||
}
|
||||
}
|
||||
(paths, skipped)
|
||||
});
|
||||
|
||||
if paths.is_empty() && skipped == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut errors = Vec::new();
|
||||
if skipped != 0 {
|
||||
errors.push(format!(
|
||||
"Skipped {skipped} dropped file(s) without a local path. Use File > Import > File..."
|
||||
));
|
||||
}
|
||||
|
||||
let (proj, rest): (Vec<String>, Vec<String>) = paths.into_iter().partition(|p| {
|
||||
let l = p.to_ascii_lowercase();
|
||||
l.ends_with(".ecproj") || l.ends_with(".ecprojx")
|
||||
});
|
||||
if let Some(p) = proj.first()
|
||||
&& let Err(e) = crate::menus::invoke(app, ctx, "file.open", json!({"path": p}))
|
||||
{
|
||||
app.ui.status = e;
|
||||
|
||||
if proj.len() > 1 {
|
||||
errors.push(format!(
|
||||
"Only one project can be opened per drop; skipped {} more",
|
||||
proj.len() - 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}"));
|
||||
}
|
||||
|
||||
if !rest.is_empty() {
|
||||
let viewer = app.auto.find("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 =
|
||||
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));
|
||||
let params =
|
||||
json!({"paths": rest, "drag": true, "importAs": app.session.prefs.drag_import_as(), "addToComp": at.is_some(), "position": at, "background": true});
|
||||
if let Err(e) = crate::menus::invoke(app, ctx, "file.import", params) {
|
||||
app.ui.status = e;
|
||||
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))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let params = json!({
|
||||
"paths": rest,
|
||||
"drag": true,
|
||||
"importAs": app.session.prefs.drag_import_as(),
|
||||
"addToComp": at.is_some(),
|
||||
"position": at,
|
||||
"background": true
|
||||
});
|
||||
|
||||
if let Err(e) = crate::menus::invoke(app, &ctx, "file.import", params) {
|
||||
errors.push(format!("Cannot import dropped files: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
app.ui.status = errors.join("; ");
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-read the system's free memory every 30 s (cache budgets follow it; on Windows each
|
||||
@@ -110,10 +191,21 @@ 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)
|
||||
@@ -122,27 +214,57 @@ 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 mut builder = egui::ViewportBuilder::default().with_title(title).with_inner_size([960.0, 540.0]);
|
||||
|
||||
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]);
|
||||
|
||||
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);
|
||||
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 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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user