Merge remote-tracking branch 'origin/main' into HEAD

This commit is contained in:
bflatastic
2026-10-30 07:04:13 +07:00
37 changed files with 958 additions and 112 deletions
+3
View File
@@ -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;
}
+62 -1
View File
@@ -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...");
}
+7
View File
@@ -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"),
+16 -4
View File
@@ -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);
+44 -2
View File
@@ -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");
}
}
+20
View File
@@ -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;
+3
View File
@@ -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);
+25 -20
View File
@@ -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 });