mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 14:08:38 -06:00
Merge remote-tracking branch 'origin/main' into HEAD
This commit is contained in:
@@ -144,6 +144,9 @@ impl EffectcraftApp {
|
||||
// commands handled here use the right catalog already.
|
||||
let language = crate::i18n::language(self);
|
||||
crate::i18n::set_current(language);
|
||||
// #1089: a font-menu rescan runs off the UI thread. Poll it on the logic tick so the
|
||||
// status and the menu update even when the window is not painting.
|
||||
crate::panels::text_panels::poll_font_rescan(self, ctx);
|
||||
if self.styled_language != language {
|
||||
self.want_paint = true;
|
||||
}
|
||||
|
||||
@@ -266,6 +266,8 @@ pub(crate) fn entry(app: &EffectcraftApp, e: &MenuEntry, shown: String) -> Strin
|
||||
"es" => Some(("Deshacer", "No se puede deshacer", "Rehacer", "No se puede rehacer")),
|
||||
"pt-br" => Some(("Desfazer", "Não há nada para desfazer", "Refazer", "Não há nada para refazer")),
|
||||
"cs" => Some(("Zpět", "Není co vrátit", "Znovu", "Není co zopakovat")),
|
||||
"fr" => Some(("Annuler", "Impossible d'annuler", "Rétablir", "Impossible de rétablir")),
|
||||
"de" => Some(("Rückgängig", "Rückgängig nicht möglich", "Wiederholen", "Wiederholen nicht möglich")),
|
||||
// A colon before the history's (untranslated) action name: "დაბრუნება: Comp1".
|
||||
"ka" => Some(("დაბრუნება:", "დაბრუნება შეუძლებელია", "ხელახლა შესრულება:", "ხელახლა შესრულება შეუძლებელია")),
|
||||
// A colon before the history's (untranslated) action name: "Отменить: Comp1".
|
||||
@@ -836,6 +838,8 @@ const SIMPLIFIED_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "行落下"),
|
||||
("anim.addKeyframe", "Add Keyframe", "添加关键帧"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "切换定格关键帧"),
|
||||
("keys.setMuted", "Mute Keyframes", "静音关键帧"),
|
||||
("keys.setMuted", "Unmute Keyframes", "取消静音关键帧"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "关键帧插值..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "关键帧速度..."),
|
||||
("", "Keyframe Assistant", "关键帧辅助"),
|
||||
@@ -1472,6 +1476,8 @@ const TRADITIONAL_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "行落下"),
|
||||
("anim.addKeyframe", "Add Keyframe", "新增關鍵影格"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "切換定格關鍵影格"),
|
||||
("keys.setMuted", "Mute Keyframes", "靜音關鍵影格"),
|
||||
("keys.setMuted", "Unmute Keyframes", "取消靜音關鍵影格"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "關鍵影格插補..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "關鍵影格速度..."),
|
||||
("", "Keyframe Assistant", "關鍵影格輔助"),
|
||||
@@ -2105,6 +2111,8 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "行をドロップイン"),
|
||||
("anim.addKeyframe", "Add Keyframe", "キーフレームを追加"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "停止したキーフレームの切り替え"),
|
||||
("keys.setMuted", "Mute Keyframes", "キーフレームをミュート"),
|
||||
("keys.setMuted", "Unmute Keyframes", "キーフレームのミュートを解除"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "キーフレーム補間法..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "キーフレーム速度..."),
|
||||
("", "Keyframe Assistant", "キーフレーム補助"),
|
||||
@@ -2741,6 +2749,8 @@ const UKRAINIAN: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Поява рядків згори"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Додати ключовий кадр"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Перемкнути утримання ключового кадру"),
|
||||
("keys.setMuted", "Mute Keyframes", "Заглушити ключові кадри"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Зняти заглушення ключових кадрів"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Інтерполяція ключових кадрів..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Швидкість ключових кадрів..."),
|
||||
("", "Keyframe Assistant", "Помічник ключових кадрів"),
|
||||
@@ -3378,6 +3388,8 @@ const SPANISH: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Entrada de líneas cayendo"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Añadir fotograma clave"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Conmutar mantener fotograma clave"),
|
||||
("keys.setMuted", "Mute Keyframes", "Silenciar fotogramas clave"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Reactivar fotogramas clave"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Interpolación de fotogramas clave..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Velocidad de fotogramas clave..."),
|
||||
("", "Keyframe Assistant", "Asistente de fotogramas clave"),
|
||||
@@ -4013,6 +4025,8 @@ const PORTUGUESE: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Entrar com queda de linhas"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Adicionar quadro-chave"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Alternar quadro-chave de retenção"),
|
||||
("keys.setMuted", "Mute Keyframes", "Silenciar quadros-chave"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Reativar quadros-chave"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Interpolação de quadro-chave..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Velocidade do quadro-chave..."),
|
||||
("", "Keyframe Assistant", "Assistente de quadro-chave"),
|
||||
@@ -4655,6 +4669,8 @@ const RUSSIAN: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Появление строк сверху"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Добавить ключевой кадр"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Переключить фиксацию ключевого кадра"),
|
||||
("keys.setMuted", "Mute Keyframes", "Отключить ключевые кадры"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Включить ключевые кадры"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Интерполяция ключевых кадров..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Скорость ключевых кадров..."),
|
||||
("", "Keyframe Assistant", "Помощник по ключевым кадрам"),
|
||||
@@ -5290,6 +5306,8 @@ const FRENCH: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Chute des lignes depuis le haut"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Ajouter une image clé"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Activer/Désactiver l'image clé figée"),
|
||||
("keys.setMuted", "Mute Keyframes", "Désactiver les images clés"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Réactiver les images clés"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Interpolation des images clés..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Vitesse des images clés..."),
|
||||
("", "Keyframe Assistant", "Assistant d'images clés"),
|
||||
@@ -5925,6 +5943,8 @@ const GERMAN: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Zeilen fallen ein"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Keyframe hinzufügen"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Halte-Keyframe umschalten"),
|
||||
("keys.setMuted", "Mute Keyframes", "Keyframes stummschalten"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Keyframes wieder aktivieren"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Keyframe-Interpolation..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Keyframe-Geschwindigkeit..."),
|
||||
("", "Keyframe Assistant", "Keyframe-Assistent"),
|
||||
@@ -6561,6 +6581,8 @@ const CZECH: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "Spustit řádky shora"),
|
||||
("anim.addKeyframe", "Add Keyframe", "Přidat klíčový snímek"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "Přepnout podržení klíčového snímku"),
|
||||
("keys.setMuted", "Mute Keyframes", "Ztlumit klíčové snímky"),
|
||||
("keys.setMuted", "Unmute Keyframes", "Obnovit klíčové snímky"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "Interpolace klíčových snímků..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "Rychlost klíčového snímku..."),
|
||||
("", "Keyframe Assistant", "Pomocník klíčových snímků"),
|
||||
@@ -7192,6 +7214,8 @@ const GEORGIAN: &[(&str, &str, &str)] = &[
|
||||
("layer.applyTextPreset", "Drop In Lines", "სტრიქონების ჩამოვარდნა"),
|
||||
("anim.addKeyframe", "Add Keyframe", "საკვანძო კადრის დამატება"),
|
||||
("keys.toggleHold", "Toggle Hold Keyframe", "ფიქსირებული საკვანძო კადრის გადართვა"),
|
||||
("keys.setMuted", "Mute Keyframes", "საკვანძო კადრების დადუმება"),
|
||||
("keys.setMuted", "Unmute Keyframes", "საკვანძო კადრების დადუმების მოხსნა"),
|
||||
("keys.interpolation", "Keyframe Interpolation...", "საკვანძო კადრის ინტერპოლაცია..."),
|
||||
("keys.velocity", "Keyframe Velocity...", "საკვანძო კადრის სიჩქარე..."),
|
||||
("", "Keyframe Assistant", "საკვანძო კადრების ასისტენტი"),
|
||||
@@ -7550,6 +7574,43 @@ mod tests {
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"en"})).unwrap();
|
||||
assert_eq!(label(&app, "", "Composition"), "Composition");
|
||||
}
|
||||
/// Match System in the browser also picks the Russian, French and German catalogs, in the
|
||||
/// order of the browser's preferences (#1700).
|
||||
#[test]
|
||||
fn browser_preferences_choose_russian_french_and_german() {
|
||||
let tags = |t: &[&str]| t.iter().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
for (locales, expected) in
|
||||
[(vec!["de-DE", "es-ES"], "de"), (vec!["xx-XX", "fr-CA"], "fr"), (vec!["ru-RU", "cs"], "ru"), (vec!["en-US", "de-DE"], "en"), (vec!["xx-XX"], "en")]
|
||||
{
|
||||
assert_eq!(super::first_supported(None, &tags(&locales), super::web_draws), expected, "{locales:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The Edit menu's Undo and Redo name the history's action in the interface language, also in
|
||||
/// the languages added with #1700.
|
||||
#[test]
|
||||
fn undo_and_redo_are_localized_for_russian_french_and_german() {
|
||||
for (language, undo, no_undo, redo, no_redo) in [
|
||||
("ru", "Отменить:", "Нечего отменять", "Повторить:", "Нечего повторять"),
|
||||
("fr", "Annuler", "Impossible d'annuler", "Rétablir", "Impossible de rétablir"),
|
||||
("de", "Rückgängig", "Rückgängig nicht möglich", "Wiederholen", "Wiederholen nicht möglich"),
|
||||
] {
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
app.session.execute("prefs.set", json!({"key": "general.language", "value": language})).unwrap();
|
||||
let entry_of = |app: &EffectcraftApp, command: &str| {
|
||||
let (_, e) = effectcraft_engine::menus::entries().into_iter().find(|(_, e)| e.command == command).unwrap();
|
||||
entry(app, e, e.label.clone())
|
||||
};
|
||||
assert_eq!(entry_of(&app, "edit.undo"), no_undo, "{language}");
|
||||
assert_eq!(entry_of(&app, "edit.redo"), no_redo, "{language}");
|
||||
app.session.execute("comp.new", json!({"name": "A", "duration": 1})).unwrap();
|
||||
let action = app.session.history.undo.last().map(|u| u.0.clone()).unwrap();
|
||||
assert_eq!(entry_of(&app, "edit.undo"), format!("{undo} {action}"), "{language}");
|
||||
app.session.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(entry_of(&app, "edit.redo"), format!("{redo} {action}"), "{language}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_system_uses_the_system_language_where_there_is_a_catalog() {
|
||||
for (locale, language) in [
|
||||
@@ -7676,7 +7737,7 @@ mod tests {
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"ru"})).unwrap();
|
||||
assert_eq!(entry(&app, &undo, "Can't Undo".into()), "Нечего отменять");
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"de"})).unwrap();
|
||||
assert_eq!(entry(&app, &undo, "Can't Undo".into()), "Can't Undo", "no German undo wording yet: the shown label");
|
||||
assert_eq!(entry(&app, &undo, "Can't Undo".into()), "Rückgängig nicht möglich");
|
||||
app.session.execute("prefs.set", json!({"key":"general.language", "value":"en"})).unwrap();
|
||||
assert_eq!(entry(&app, &new_comp, new_comp.label.clone()), "New Composition...");
|
||||
}
|
||||
|
||||
@@ -434,6 +434,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("No layer selected", "レイヤーが選択されていません"),
|
||||
("No matching effects or presets", "一致するエフェクトまたはプリセットはありません"),
|
||||
("No matching fonts", "一致するフォントはありません"),
|
||||
("Rescan Fonts", "フォントを再スキャン"),
|
||||
("Variable fonts only", "可変フォントのみ"),
|
||||
("No model credits were built into this copy.", "このビルドにはモデルのクレジットが含まれていません。"),
|
||||
("No nested compositions", "ネストされたコンポジションはありません"),
|
||||
@@ -1167,6 +1168,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
("No layer selected", "未选择图层"),
|
||||
("No matching effects or presets", "没有匹配的效果或预设"),
|
||||
("No matching fonts", "没有匹配的字体"),
|
||||
("Rescan Fonts", "重新扫描字体"),
|
||||
("Variable fonts only", "仅可变字体"),
|
||||
("No model credits were built into this copy.", "此副本未内置模型致谢。"),
|
||||
("No nested compositions", "没有嵌套合成"),
|
||||
@@ -1951,6 +1953,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
("No layer selected", "未選取圖層"),
|
||||
("No matching effects or presets", "沒有匹配的效果或預設集"),
|
||||
("No matching fonts", "沒有符合的字體"),
|
||||
("Rescan Fonts", "重新掃描字型"),
|
||||
("Variable fonts only", "僅可變字型"),
|
||||
("No model credits were built into this copy.", "此副本未內置模型致謝。"),
|
||||
("No nested compositions", "沒有嵌套合成"),
|
||||
@@ -2695,6 +2698,7 @@ const UKRAINIAN: &[(&str, &str)] = &[
|
||||
("No layer selected", "Шар не виділено"),
|
||||
("No matching effects or presets", "Не знайдено відповідних ефектів або наборів"),
|
||||
("No matching fonts", "Не знайдено відповідних шрифтів"),
|
||||
("Rescan Fonts", "Повторно сканувати шрифти"),
|
||||
("Variable fonts only", "Лише змінні шрифти"),
|
||||
("No model credits were built into this copy.", "Ця копія не містить відомостей про моделі."),
|
||||
("No nested compositions", "Немає вкладених композицій"),
|
||||
@@ -3230,6 +3234,7 @@ const PORTUGUESE: &[(&str, &str)] = &[
|
||||
("New", "Novo"),
|
||||
("New Comp from Selection", "Nova composição a partir da seleção"),
|
||||
("Replace with Precomposition", "Substituir por pré-composição"),
|
||||
("Rescan Fonts", "Reanalisar fontes"),
|
||||
("used once", "usado uma vez"),
|
||||
("used {} times", "usado {} vezes"),
|
||||
("Compositions that use this item", "Composições que usam este item"),
|
||||
@@ -3991,6 +3996,7 @@ const CZECH: &[(&str, &str)] = &[
|
||||
("New", "Nový"),
|
||||
("New Comp from Selection", "Nová kompozice z výběru"),
|
||||
("Replace with Precomposition", "Nahradit předkompozicí"),
|
||||
("Rescan Fonts", "Znovu nalézt písma"),
|
||||
("used once", "použito jednou"),
|
||||
("used {} times", "použito {}×"),
|
||||
("Compositions that use this item", "Kompozice, které tuto položku používají"),
|
||||
@@ -4575,6 +4581,7 @@ const SPANISH: &[(&str, &str)] = &[
|
||||
("No layer selected", "No hay ninguna capa seleccionada"),
|
||||
("No matching effects or presets", "No hay efectos ni ajustes preestablecidos que coincidan"),
|
||||
("No matching fonts", "No hay fuentes coincidentes"),
|
||||
("Rescan Fonts", "Volver a analizar fuentes"),
|
||||
("Variable fonts only", "Solo fuentes variables"),
|
||||
("No model credits were built into this copy.", "No se incluyeron créditos de modelos en esta copia."),
|
||||
("No nested compositions", "No hay composiciones anidadas"),
|
||||
|
||||
@@ -48,9 +48,21 @@ struct KeyPt {
|
||||
t: f64,
|
||||
v: f64,
|
||||
sel: bool,
|
||||
muted: bool,
|
||||
handles: Vec<Handle>,
|
||||
}
|
||||
|
||||
/// A graph key's fill and edge colours: muted keys stay visible and selectable, dimmed.
|
||||
fn key_colors(t: &Tokens, edge: Color32, k: &KeyPt) -> (Color32, Color32) {
|
||||
let fill = match (k.muted, k.sel) {
|
||||
(true, true) => t.keyframe_selected.gamma_multiply(0.5),
|
||||
(true, false) => Color32::from_gray(0x70),
|
||||
(false, true) => t.keyframe_selected,
|
||||
(false, false) => Color32::from_gray(0xe0),
|
||||
};
|
||||
(fill, if k.muted { edge.gamma_multiply(0.4) } else { edge })
|
||||
}
|
||||
|
||||
struct Curve<'a> {
|
||||
layer: &'a Layer,
|
||||
prop: &'a Property,
|
||||
@@ -153,7 +165,7 @@ fn curves<'a>(app: &EffectcraftApp, comp: &'a Comp, ectx: &EvalCtx, tm: TMap, pl
|
||||
let dt = e.influence * dur * if out_side { 1.0 } else { -1.0 };
|
||||
let (hv, show) = if speed { (e.speed.abs(), true) } else { (k.value.components().get(d).copied().unwrap_or(0.0) + e.speed * dt, !spatial) };
|
||||
let bezier = if out_side { k.out_interp == Interp::Bezier } else { k.in_interp == Interp::Bezier };
|
||||
if ks && show && (bezier || speed) {
|
||||
if ks && !k.muted && show && (bezier || speed) {
|
||||
handles.push(Handle { out: out_side, t: kt + dt, v: hv, speed: e.speed, dur });
|
||||
}
|
||||
}
|
||||
@@ -162,7 +174,7 @@ fn curves<'a>(app: &EffectcraftApp, comp: &'a Comp, ectx: &EvalCtx, tm: TMap, pl
|
||||
} else {
|
||||
k.value.components().get(d).copied().unwrap_or(0.0)
|
||||
};
|
||||
keys.push(KeyPt { idx: i, time: k.time, t: kt, v, sel: ks, handles });
|
||||
keys.push(KeyPt { idx: i, time: k.time, t: kt, v, sel: ks, muted: k.muted, handles });
|
||||
}
|
||||
out.push(Curve { layer: l, prop: pr, dim: d, color, pts, keys, editable: !spatial });
|
||||
}
|
||||
@@ -522,9 +534,9 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte
|
||||
}
|
||||
}
|
||||
// The key itself.
|
||||
let col = if k.sel { t.keyframe_selected } else { Color32::from_gray(0xe0) };
|
||||
let (col, edge) = key_colors(&t, c.color, k);
|
||||
pc.rect_filled(Rect::from_center_size(kp, vec2(7.0, 7.0)), 0.0, col);
|
||||
pc.rect_stroke(Rect::from_center_size(kp, vec2(7.0, 7.0)), 0.0, Stroke::new(1.0, c.color), StrokeKind::Outside);
|
||||
pc.rect_stroke(Rect::from_center_size(kp, vec2(7.0, 7.0)), 0.0, Stroke::new(1.0, edge), StrokeKind::Outside);
|
||||
let kr = Rect::from_center_size(kp, vec2(11.0, 11.0));
|
||||
let kresp = ui.interact(kr, egui::Id::unique(("gkey", uid, d, k.idx)), Sense::click_and_drag());
|
||||
app.auto.add(&format!("timeline.graph.key.{uid}.{d}.{}", k.idx), kr, &c.prop.name);
|
||||
|
||||
@@ -451,6 +451,22 @@ fn queue_drop(app: &EffectcraftApp, ui: &egui::Ui, rect: Rect, p: &egui::Painter
|
||||
}
|
||||
}
|
||||
|
||||
/// Elapsed and estimated remaining seconds at UI time `now`. The job reports them as frames
|
||||
/// finish; between reports they count on from the last one, as a clock does, so a slow frame
|
||||
/// doesn't read as a stalled render (#2593).
|
||||
fn live_times(ctx: &egui::Context, pr: &effectcraft_engine::render_queue::JobState, now: f64) -> (f64, Option<f64>) {
|
||||
let report = (pr.current, pr.done, pr.item_elapsed.to_bits());
|
||||
let since = ctx.data_mut(|d| {
|
||||
let seen = d.get_temp_mut_or_insert_with(egui::Id::unique("rq-progress-seen"), || (report, now));
|
||||
if seen.0 != report {
|
||||
*seen = (report, now);
|
||||
}
|
||||
seen.1
|
||||
});
|
||||
let dt = if pr.finished { 0.0 } else { (now - since).max(0.0) };
|
||||
(pr.item_elapsed + dt, pr.remaining.map(|r| (r - dt).max(0.0)))
|
||||
}
|
||||
|
||||
/// The Current Render strip inside `top`: title, elapsed / remaining time, Render, Stop, Notify
|
||||
/// and the progress bar.
|
||||
fn current_render_strip(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, top: Rect, rq: &RqView, actions: &mut Vec<Action>) {
|
||||
@@ -461,11 +477,15 @@ fn current_render_strip(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::P
|
||||
let cur_item = progress.as_ref().and_then(|p| p.current).and_then(|id| queue.iter().find(|i| i.id == id));
|
||||
let cur_name = cur_item.and_then(|i| app.session.project.item(i.comp)).map(|i| i.name.clone());
|
||||
if let Some(pr) = &progress {
|
||||
let rem = pr.remaining.map(hms).unwrap_or_else(|| "–".into());
|
||||
let (elapsed, remaining) = live_times(ui.ctx(), pr, ui.input(|i| i.time));
|
||||
if rendering {
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_millis(500));
|
||||
}
|
||||
let rem = remaining.map(hms).unwrap_or_else(|| "–".into());
|
||||
p.text(
|
||||
pos2(top.max.x - 12.0, top.min.y + 16.0),
|
||||
Align2::RIGHT_CENTER,
|
||||
format!("Elapsed: {} Est. Remain: {rem}", hms(pr.item_elapsed)),
|
||||
format!("Elapsed: {} Est. Remain: {rem}", hms(elapsed)),
|
||||
Tokens::ui(11.5),
|
||||
t.text_dim,
|
||||
);
|
||||
@@ -903,4 +923,26 @@ mod tests {
|
||||
assert_eq!(super::utc_stamp(u64::MAX), "584554051223-11-09 07:00:15");
|
||||
assert_eq!(super::hms(3725.4), "1:02:05");
|
||||
}
|
||||
|
||||
/// #2593: between progress reports the times count on from the last report, and a new
|
||||
/// report starts the count again.
|
||||
#[test]
|
||||
fn render_times_tick_between_reports() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut pr = effectcraft_engine::render_queue::JobState {
|
||||
current: Some(1),
|
||||
done: 2,
|
||||
total: 10,
|
||||
item_elapsed: 20.0,
|
||||
remaining: Some(80.0),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(super::live_times(&ctx, &pr, 100.0), (20.0, Some(80.0)));
|
||||
assert_eq!(super::live_times(&ctx, &pr, 103.0), (23.0, Some(77.0)));
|
||||
assert_eq!(super::live_times(&ctx, &pr, 300.0).1, Some(0.0), "never below zero");
|
||||
(pr.done, pr.item_elapsed, pr.remaining) = (3, 31.0, Some(72.0));
|
||||
assert_eq!(super::live_times(&ctx, &pr, 301.0), (31.0, Some(72.0)));
|
||||
pr.finished = true;
|
||||
assert_eq!(super::live_times(&ctx, &pr, 400.0), (31.0, Some(72.0)), "a finished job stands still");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,6 +654,17 @@ struct FontSearch {
|
||||
}
|
||||
|
||||
/// Shared font menu for Character and Properties: search, recent fonts and previews.
|
||||
/// Finish a font rescan started from the font menu and report how many faces it added.
|
||||
/// While the scan is still walking font folders, ask for another frame so the menu updates.
|
||||
pub(crate) fn poll_font_rescan(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
if effectcraft_engine::text::fonts::rescan_busy() {
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(100));
|
||||
}
|
||||
if let Some(n) = effectcraft_engine::text::fonts::poll_rescan() {
|
||||
app.ui.status = format!("{} +{n}", tr("Rescan Fonts"));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn font_picker(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui::Id, rect: Rect, current: &str, auto: &str, enabled: bool) -> Option<String> {
|
||||
let state_id = id.with("search-state");
|
||||
app.auto.add(auto, rect, "Font family");
|
||||
@@ -724,6 +735,15 @@ pub(super) fn font_picker(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui:
|
||||
let boxed = widgets::checkbox(ui, box_rect, only_variable, &t, id.with("variable-check"));
|
||||
ui.painter().text(label_rect.left_center(), Align2::LEFT_CENTER, tr("Variable fonts only"), Tokens::ui(11.5), t.text);
|
||||
app_ref.auto.add(&format!("{auto}.variable"), row.rect, "Variable fonts only");
|
||||
// #1089: fonts installed after launch appear without restarting. The button is its own
|
||||
// row: the popup is only 180 px wide, so a control pinned to the filter row covers
|
||||
// "Variable fonts only". The scan itself runs off the UI thread (`poll_font_rescan`).
|
||||
let rescan_h = 22.0;
|
||||
let (resc, _) = ui.allocate_exact_size(vec2(ui.available_width(), rescan_h), egui::Sense::hover());
|
||||
if widgets::text_button(ui, resc, tr("Rescan Fonts"), false, &t, id.with("rescan")).clicked() && effectcraft_engine::text::fonts::start_rescan() {
|
||||
app_ref.ui.status = format!("{}…", tr("Rescan Fonts"));
|
||||
}
|
||||
app_ref.auto.add(&format!("{auto}.rescan"), resc, "Rescan Fonts");
|
||||
let toggled = row.clicked() || boxed.clicked();
|
||||
if toggled {
|
||||
only_variable = !only_variable;
|
||||
|
||||
@@ -4111,6 +4111,7 @@ fn key_context_menu(app: &mut EffectcraftApp, g: &mut Rw, rc: &RowCtx<'_>, prop:
|
||||
("Keyframe Interpolation…", "keys.interpolation", json!({})),
|
||||
("Keyframe Velocity…", "keys.velocity", json!({})),
|
||||
("Toggle Hold Keyframe", "keys.toggleHold", json!({})),
|
||||
(if k.muted { "Unmute Keyframes" } else { "Mute Keyframes" }, "keys.setMuted", json!({"muted": !k.muted})),
|
||||
("Rove Across Time", "keys.interpolation", json!({"roving": !k.roving})),
|
||||
("-", "", json!({})),
|
||||
("Easy Ease", "keys.easyEase", json!({})),
|
||||
@@ -4203,6 +4204,8 @@ fn key_paint(g: &Rw, rc: &RowCtx<'_>, k: &effectcraft_engine::keyframe::Keyframe
|
||||
None if ks => t.keyframe_selected,
|
||||
None => t.keyframe,
|
||||
};
|
||||
// Muted keys stay visible and editable but no longer drive the value.
|
||||
let kcol = if k.muted { kcol.gamma_multiply(0.35) } else { kcol };
|
||||
if k.roving {
|
||||
// Roving keys: a small dot (their time follows their neighbours).
|
||||
gp.circle_filled(c, 2.5, kcol);
|
||||
|
||||
@@ -360,31 +360,36 @@ pub(crate) fn motion_path(
|
||||
}
|
||||
let v = k.value.as_vec3();
|
||||
let s = to_scr(v);
|
||||
// Auto-Bezier keys show handles on a side the path leaves straight too (a two-key path's
|
||||
// ends), to drag it into a curve; Linear keys (no tangents) show none.
|
||||
let (tin, tout) = if k.spatial_auto {
|
||||
effectcraft_engine::keyframe::spatial_handles(&pos.keys, i)
|
||||
} else {
|
||||
effectcraft_engine::keyframe::spatial_tangents(&pos.keys, i)
|
||||
};
|
||||
for (out, tg) in [(false, tin), (true, tout)] {
|
||||
let has = (out && i + 1 < pos.keys.len()) || (!out && i > 0);
|
||||
if !has || (tg[0] == 0.0 && tg[1] == 0.0) {
|
||||
continue;
|
||||
let selected = sel.contains(&KeyRef { layer: l.id, prop: pos.uid, time: k.time });
|
||||
// A muted position key is off the motion path: no tangents, and a dimmed point.
|
||||
let kc = if k.muted { col.gamma_multiply(if selected { 0.55 } else { 0.35 }) } else { col };
|
||||
if !k.muted {
|
||||
// Auto-Bezier keys show handles on a side the path leaves straight too (a two-key path's
|
||||
// ends), to drag it into a curve; Linear keys (no tangents) show none.
|
||||
let (tin, tout) = if k.spatial_auto {
|
||||
effectcraft_engine::keyframe::spatial_handles(&pos.keys, i)
|
||||
} else {
|
||||
effectcraft_engine::keyframe::spatial_tangents(&pos.keys, i)
|
||||
};
|
||||
for (out, tg) in [(false, tin), (true, tout)] {
|
||||
let has = (out && i + 1 < pos.keys.len()) || (!out && i > 0);
|
||||
if !has || (tg[0] == 0.0 && tg[1] == 0.0) {
|
||||
continue;
|
||||
}
|
||||
let h = to_scr([v[0] + tg[0], v[1] + tg[1], v[2] + tg[2]]);
|
||||
painter.line_segment([s, h], Stroke::new(1.0, kc));
|
||||
painter.circle_filled(h, 3.0, kc);
|
||||
let side = if out { "out" } else { "in" };
|
||||
app.auto.add(&format!("viewer.motionPath.{}.{i}.{side}", l.id.0), Rect::from_center_size(h, vec2(8.0, 8.0)), "Spatial tangent");
|
||||
hits.push(KeyHit { layer: l.id, prop: pos.uid, time: k.time, pos: h, tangent: Some(out), value: v, p2c });
|
||||
}
|
||||
let h = to_scr([v[0] + tg[0], v[1] + tg[1], v[2] + tg[2]]);
|
||||
painter.line_segment([s, h], Stroke::new(1.0, col));
|
||||
painter.circle_filled(h, 3.0, col);
|
||||
let side = if out { "out" } else { "in" };
|
||||
app.auto.add(&format!("viewer.motionPath.{}.{i}.{side}", l.id.0), Rect::from_center_size(h, vec2(8.0, 8.0)), "Spatial tangent");
|
||||
hits.push(KeyHit { layer: l.id, prop: pos.uid, time: k.time, pos: h, tangent: Some(out), value: v, p2c });
|
||||
}
|
||||
let r = Rect::from_center_size(s, vec2(7.0, 7.0));
|
||||
if sel.contains(&KeyRef { layer: l.id, prop: pos.uid, time: k.time }) {
|
||||
painter.rect_filled(r, 0.0, col);
|
||||
if selected {
|
||||
painter.rect_filled(r, 0.0, kc);
|
||||
} else {
|
||||
painter.rect_filled(r, 0.0, Color32::from_black_alpha(140));
|
||||
painter.rect_stroke(r, 0.0, Stroke::new(1.0, col), StrokeKind::Inside);
|
||||
painter.rect_stroke(r, 0.0, Stroke::new(1.0, kc), StrokeKind::Inside);
|
||||
}
|
||||
app.auto.add(&format!("viewer.motionPath.{}.{i}", l.id.0), r.expand(2.0), "Position keyframe");
|
||||
hits.push(KeyHit { layer: l.id, prop: pos.uid, time: k.time, pos: s, tangent: None, value: v, p2c });
|
||||
|
||||
Reference in New Issue
Block a user