G9: integrate prerequisite panel label wiring (#1581)

This commit is contained in:
Ikki Nishihara committed 2026-10-18 13:22:26 +09:00
commit b333f08728
17 files changed
+192 -30

No files matched your search

+16 -3
View File
@@ -39,11 +39,24 @@ impl EffectcraftApp {
out.push((panels::viewers::panel(id), t));
}
}
let Some(comp) = self.session.active_comp() else { return out };
if let Some(l) = self.session.state.selected_layers.first().and_then(|id| comp.layer(*id)) {
if let Some(comp) = self.session.active_comp()
&& let Some(l) = self.session.state.selected_layers.first().and_then(|id| comp.layer(*id))
{
out.push((PanelKind::EffectControls, format!("Effect Controls {}", l.name)));
out.push((PanelKind::Properties, format!("Properties: {}", l.name)));
}
// The panels named for themselves, in the interface language. The Window menu has carried a
// (`window.panel`, title) row for these in every catalog that exists, so the dock reads the
// same pair instead of drawing `title()`'s English; a panel the menu does not list (`History`,
// `ScriptUI Panel`) keeps its English name, and a tab that names a comp or a layer is listed
// above and wins, being found first.
for p in PanelKind::ALL {
// The Window menu lists most of these; `Markers` is only in the panel catalog, and the
// two panels in neither (`History`, `ScriptUI Panel`) keep their English names.
let listed = crate::i18n::label(self, "window.panel", p.title());
let label = if listed == p.title() { crate::i18n::tr(p.title()) } else { listed };
out.push((p, label.to_string()));
}
out
}
@@ -570,7 +583,7 @@ impl EffectcraftApp {
}
}
// Ghost of the dragged tab.
let g = painter.layout_no_wrap(p.title().to_string(), crate::theme::Tokens::ui(12.0), Color32::WHITE);
let g = painter.layout_no_wrap(crate::i18n::label(self, "window.panel", p.title()).to_string(), crate::theme::Tokens::ui(12.0), Color32::WHITE);
let gr = Rect::from_min_size(pos + vec2(12.0, 10.0), g.size() + vec2(16.0, 8.0));
painter.rect_filled(gr, 4.0, t.focus.gamma_multiply(0.85));
painter.galley(gr.min + vec2(8.0, 4.0), g, Color32::WHITE);
+29
View File
@@ -6961,6 +6961,35 @@ mod tests {
}
}
/// The dock's tab titles are `PanelKind::title()`; the panels the Window menu lists are drawn in
/// the interface language through their `(window.panel, title)` rows. `History` and `ScriptUI
/// Panel` are in no menu and keep their English names, which is what this pins.
#[test]
fn the_dock_draws_every_panel_it_can_in_the_interface_language() {
use crate::dock::PanelKind;
const WITHOUT_A_ROW: &[&str] = &["History", "ScriptUI Panel"];
for (name, catalog, code) in [("ja", JAPANESE, "ja"), ("zh-hans", SIMPLIFIED_CHINESE, "zh-hans"), ("zh-hant", TRADITIONAL_CHINESE, "zh-hant")] {
// The dock asks both catalogs, so a title may be named by either.
ui::set_current(code);
for p in PanelKind::ALL {
let title = p.title();
let in_menu = catalog.iter().any(|(c, en, _)| *c == "window.panel" && *en == title);
let in_panel = ui::tr(title) != title;
assert_eq!(
in_menu || in_panel,
!WITHOUT_A_ROW.contains(&title),
"{name}: {title} is {}named by a catalog and {}in the exception list",
if in_menu || in_panel { "" } else { "not " },
if WITHOUT_A_ROW.contains(&title) { "" } else { "not " }
);
}
}
ui::set_current("en");
// The dock asks the catalog rather than drawing the English name.
let dock_ui = include_str!("dock_ui.rs");
assert!(dock_ui.contains(r#"crate::i18n::label(self, "window.panel", p.title())"#), "dock_ui.rs no longer asks the catalog for the panel titles");
}
#[test]
fn zh_hans_translates_labels_computed_titles_and_keeps_names_verbatim() {
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
+113
View File
@@ -1370,6 +1370,21 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
("Total Offset", "总偏移"),
("Per Layer", "每个图层"),
("Track", "跟踪"),
("Apply", "应用"),
("Analyze", "分析"),
("Create Camera", "创建摄像机"),
("Add Point", "添加点"),
("Remove Point", "移除点"),
("Render", "渲染"),
("Save Current as Template…", "将当前项存为模板…"),
("New Composition From Footage", "从素材新建合成"),
("Search effects and presets", "搜索效果和预设"),
("Search project", "搜索项目"),
("Search", "搜索"),
("(no composition)", "没有合成"),
("per second", "每秒"),
("Waveform", "波形"),
("steps", "步"),
];
const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
@@ -2099,6 +2114,21 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
("Total Offset", "總偏移"),
("Per Layer", "每個圖層"),
("Track", "追蹤"),
("Apply", "套用"),
("Analyze", "分析"),
("Create Camera", "建立攝影機"),
("Add Point", "添加點"),
("Remove Point", "移除點"),
("Render", "算圖"),
("Save Current as Template…", "將目前項目存為範本…"),
("New Composition From Footage", "從素材新增合成"),
("Search effects and presets", "搜尋效果與預設"),
("Search project", "搜尋專案"),
("Search", "搜尋"),
("(no composition)", "沒有合成"),
("per second", "每秒"),
("Waveform", "波形"),
("steps", "步驟"),
];
/// Ukrainian (uk): original translations of the same interface rows as the Chinese catalogs.
@@ -4478,6 +4508,89 @@ mod tests {
}
}
/// Labels drawn through a helper's *argument* or through `Painter::text` are invisible to the
/// scan above, which only looks at a literal right after the call token: a `widgets::` helper
/// takes its label as its second or third argument, and `Painter::text` takes the string third.
/// Both shapes are checked here, so an English literal there is caught like any other.
#[test]
fn helper_and_painter_labels_are_translated() {
/// The `widgets.rs` helpers that draw a label, and which argument holds it.
const HELPER_LABELS: &[(&str, usize)] =
&[("text_button", 2), ("search_field", 3), ("text_field", 3), ("dropdown", 2), ("dropdown_tip", 2), ("text_fit", 3), ("check_label", 1)];
/// Literals that draw as they are: a brand, a value, a separator, a unit, and the kerning
/// sample pair the Character panel shows.
const VERBATIM: &[&str] = &["EffectCraft", "32", "dB", "···", "×", "V/A"];
/// The top-level arguments of a call, given the text after its `(`.
fn args_of(s: &str) -> Vec<&str> {
let (mut out, mut depth, mut start, mut instr) = (Vec::new(), 0i32, None, false);
for (i, c) in s.char_indices() {
match c {
'"' if !instr => {
instr = true;
if start.is_none() {
start = Some(i);
}
}
'"' => instr = false,
'(' | '[' | '{' if !instr => depth += 1,
')' | ']' | '}' if !instr => {
if depth == 0 {
break;
}
depth -= 1;
}
',' if !instr && depth == 0 => {
if let Some(st) = start {
out.push(s[st..i].trim());
}
start = None;
}
c if !instr && !c.is_whitespace() && start.is_none() => start = Some(i),
_ => {}
}
}
if let Some(st) = start {
out.push(s[st..].trim());
}
out
}
for (path, source) in CONVERTED {
// The test modules of these files draw nothing; they hold sample strings.
let source = source.split("#[cfg(test)]").next().unwrap_or(source);
for (n, line) in source.lines().enumerate() {
let code = line.split("//").next().unwrap_or(line);
let check = |args: Vec<&str>, index: usize, what: &str| {
let Some(arg) = args.get(index) else { return };
// A single character is a symbol or a glyph, not interface text - the same
// exemption the call-shape scan above makes.
let inner = arg.strip_prefix('"').and_then(|a| a.strip_suffix('"')).filter(|i| !i.contains('"'));
match inner {
Some(text) if text.chars().count() >= 2 && !VERBATIM.contains(&text) => {
panic!("{path}:{}: the interface text {arg} that {what} draws is not in the catalog", n + 1);
}
_ => {}
}
};
for (call, index) in HELPER_LABELS {
let marker = format!("widgets::{call}(");
let mut from = 0;
while let Some(at) = code[from..].find(&marker) {
let at = from + at + marker.len();
check(args_of(&code[at..]), *index, &format!("widgets::{call}"));
from = at;
}
}
let mut from = 0;
while let Some(at) = code[from..].find(".text(") {
from = at + 6;
check(args_of(&code[from..]), 2, "Painter::text");
}
}
}
}
/// The Settings pages come from the engine as data, so the literal scan above cannot see them:
/// every title, section, label, choice, button, note and unit they draw needs a Chinese row
/// (#1178). Language names are written in their own language, and a few labels read the same
+4 -4
View File
@@ -109,7 +109,7 @@ pub fn wiggler(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
app.ui.anim_tools = st.clone();
let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0));
let enabled = app.session.state.selected_keys.len() >= 2;
if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::new("wg-apply")).clicked() {
if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::new("wg-apply")).clicked() {
let params = json!({
"apply": if st.wiggle_spatial { "spatial" } else { "temporal" },
"noise": if st.wiggle_smooth { "smooth" } else { "jagged" },
@@ -150,7 +150,7 @@ pub fn smoother(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
y += ROW + 8.0;
let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0));
let enabled = app.session.state.selected_keys.len() >= 3;
if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::new("sm-apply")).clicked() {
if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::new("sm-apply")).clicked() {
apply(app, "keys.smooth", json!({"tolerance": tol}));
}
app.auto.add("smoother.apply", b, "Apply");
@@ -321,7 +321,7 @@ pub fn mask_interpolation(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rec
changes.insert("keyframeRate".into(), json!(nr));
}
} else {
p.text(pos2(xv + 78.0, y + 9.0), Align2::LEFT_CENTER, "per second", Tokens::ui(12.0), t.text_dim);
p.text(pos2(xv + 78.0, y + 9.0), Align2::LEFT_CENTER, crate::i18n::tr("per second"), Tokens::ui(12.0), t.text_dim);
}
y += ROW;
let mut check = |app: &mut EffectcraftApp, y: f32, key: &str, text: &str, on: bool| {
@@ -392,7 +392,7 @@ pub fn mask_interpolation(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rec
}
let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0));
let enabled = app.session.state.selected_keys.len() >= 2;
if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::new("mi-apply")).clicked() {
if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::new("mi-apply")).clicked() {
match app.session.execute("mask.interpolate", json!({})) {
Ok(r) => app.ui.status = format!("Mask Interpolation: {} keyframes added ({} vertices)", r["keys"], r["vertices"]),
Err(e) => app.ui.status = e.to_string(),
@@ -413,12 +413,12 @@ pub fn editor(
let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0));
let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(84.0, 22.0));
let b3 = Rect::from_min_size(pos2(b2.max.x + 8.0, b1.min.y), vec2(110.0, 22.0));
if widgets::text_button(ui, b1, "Analyze", false, &t, egui::Id::new(("ct-analyze", g.uid))).clicked() && !busy {
if widgets::text_button(ui, b1, crate::i18n::tr("Analyze"), false, &t, egui::Id::new(("ct-analyze", g.uid))).clicked() && !busy {
actions.push(("camera.analyze".into(), me.clone()));
}
app.auto.add(&format!("effectControls.cameraTracker.{}.analyze", g.uid), b1, "Analyze");
if running {
if widgets::text_button(ui, b2, "Cancel", false, &t, egui::Id::new(("ct-cancel", g.uid))).clicked() {
if widgets::text_button(ui, b2, crate::i18n::tr("Cancel"), false, &t, egui::Id::new(("ct-cancel", g.uid))).clicked() {
actions.push(("camera.cancel".into(), json!({})));
}
app.auto.add(&format!("effectControls.cameraTracker.{}.cancel", g.uid), b2, "Cancel");
@@ -426,7 +426,7 @@ pub fn editor(
let params = effectcraft_engine::camera_track::static_params(g);
let solve = ct::solve(&params);
if solve.is_some() {
if widgets::text_button(ui, b3, "Create Camera", false, &t, egui::Id::new(("ct-camera", g.uid))).clicked() {
if widgets::text_button(ui, b3, crate::i18n::tr("Create Camera"), false, &t, egui::Id::new(("ct-camera", g.uid))).clicked() {
actions.push(("camera.create".into(), me.clone()));
}
app.auto.add(&format!("effectControls.cameraTracker.{}.createCamera", g.uid), b3, "Create Camera");
+2 -2
View File
@@ -756,12 +756,12 @@ fn warp_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, l
let busy = app.session.is_warp_analyzing();
let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0));
let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(84.0, 22.0));
if widgets::text_button(ui, b1, "Analyze", false, &t, egui::Id::new(("warp-analyze", g.uid))).clicked() && !busy {
if widgets::text_button(ui, b1, crate::i18n::tr("Analyze"), false, &t, egui::Id::new(("warp-analyze", g.uid))).clicked() && !busy {
actions.push(("warp.analyze".into(), json!({"layer": layer.id.0, "effect": g.uid})));
}
app.auto.add(&format!("effectControls.warp.{}.analyze", g.uid), b1, "Analyze");
if running {
if widgets::text_button(ui, b2, "Cancel", false, &t, egui::Id::new(("warp-cancel", g.uid))).clicked() {
if widgets::text_button(ui, b2, crate::i18n::tr("Cancel"), false, &t, egui::Id::new(("warp-cancel", g.uid))).clicked() {
actions.push(("warp.cancel".into(), json!({})));
}
app.auto.add(&format!("effectControls.warp.{}.cancel", g.uid), b2, "Cancel");
+1 -1
View File
@@ -277,7 +277,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
let sr = Rect::from_min_size(rect.min + vec2(8.0, 8.0), vec2(rect.width() - 44.0, 24.0));
let mut q = app.ui.effects_search.clone();
widgets::search_field(ui, sr, &mut q, "Search effects and presets", &t);
widgets::search_field(ui, sr, &mut q, crate::i18n::tr("Search effects and presets"), &t);
app.ui.effects_search = q.clone();
app.auto.add("effects.search", sr, "Search");
// Contents menu.
+1 -1
View File
@@ -250,7 +250,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let p = ui.painter().with_clip_rect(rect);
p.rect_filled(rect, 0.0, t.tl_bg);
let Some(rid) = root(app) else {
p.text(rect.center(), Align2::CENTER_CENTER, "(no composition)", Tokens::ui(12.0), t.text_faint);
p.text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("(no composition)"), Tokens::ui(12.0), t.text_faint);
return;
};
// ---- toolbar: switches and direction.
+3 -3
View File
@@ -786,19 +786,19 @@ fn reshape_buttons(
let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0));
let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(96.0, 22.0));
let b3 = Rect::from_min_size(pos2(b2.max.x + 8.0, b1.min.y), vec2(56.0, 22.0));
if widgets::text_button(ui, b1, "Add Point", false, &t, egui::Id::new(("reshape-add", euid))).clicked() {
if widgets::text_button(ui, b1, crate::i18n::tr("Add Point"), false, &t, egui::Id::new(("reshape-add", euid))).clicked() {
let mut v = pairs.clone();
v.push(add_pair(&pairs));
set_str(actions, layer, g, "correspondencePoints", format_pairs(&v), None);
}
app.auto.add(&format!("effectControls.effect.{euid}.reshape.add"), b1, "Add Point");
if widgets::text_button(ui, b2, "Remove Point", false, &t, egui::Id::new(("reshape-remove", euid))).clicked() && !pairs.is_empty() {
if widgets::text_button(ui, b2, crate::i18n::tr("Remove Point"), false, &t, egui::Id::new(("reshape-remove", euid))).clicked() && !pairs.is_empty() {
let mut v = pairs.clone();
v.pop();
set_str(actions, layer, g, "correspondencePoints", format_pairs(&v), None);
}
app.auto.add(&format!("effectControls.effect.{euid}.reshape.remove"), b2, "Remove Point");
if widgets::text_button(ui, b3, "Clear", false, &t, egui::Id::new(("reshape-clear", euid))).clicked() && !pairs.is_empty() {
if widgets::text_button(ui, b3, crate::i18n::tr("Clear"), false, &t, egui::Id::new(("reshape-clear", euid))).clicked() && !pairs.is_empty() {
set_str(actions, layer, g, "correspondencePoints", String::new(), None);
}
app.auto.add(&format!("effectControls.effect.{euid}.reshape.clear"), b3, "Clear");
+1 -1
View File
@@ -38,7 +38,7 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
p.circle_filled(badge.center(), 22.0, t.accent.gamma_multiply(0.25));
p.circle_stroke(badge.center(), 22.0, Stroke::new(1.5, t.accent));
p.text(badge.center() - vec2(0.0, 5.0), Align2::CENTER_CENTER, tut.steps.len().to_string(), Tokens::semibold(15.0), Color32::WHITE);
p.text(badge.center() + vec2(0.0, 10.0), Align2::CENTER_CENTER, "steps", Tokens::ui(9.0), t.text_dim);
p.text(badge.center() + vec2(0.0, 10.0), Align2::CENTER_CENTER, crate::i18n::tr("steps"), Tokens::ui(9.0), t.text_dim);
let tx = badge.max.x + 16.0;
p.text(pos2(tx, r.min.y + 22.0), Align2::LEFT_CENTER, &tut.title, Tokens::semibold(14.0), Color32::WHITE);
p.text(pos2(tx, r.min.y + 40.0), Align2::LEFT_CENTER, format!("{} min", tut.minutes), Tokens::ui(11.0), t.text_faint);
+2 -1
View File
@@ -11,7 +11,8 @@ use crate::{EffectcraftApp, widgets};
pub fn placeholder(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect, p: PanelKind) {
let t = app.tokens;
ui.painter().text(rect.center(), Align2::CENTER_CENTER, tr_args("{} — coming soon", &[&(p.title())]), Tokens::ui(12.0), t.text_faint);
let name = crate::i18n::label(app, "window.panel", p.title());
ui.painter().text(rect.center(), Align2::CENTER_CENTER, tr_args("{} — coming soon", &[&name]), Tokens::ui(12.0), t.text_faint);
}
/// Preview panel: transport controls, the shortcut whose options are shown, and that
+1 -1
View File
@@ -240,7 +240,7 @@ pub fn panel_menu_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
}
}
_ => {
ui.label(egui::RichText::new(panel.title()).weak());
ui.label(egui::RichText::new(crate::i18n::label(app, "window.panel", panel.title())).weak());
}
}
});
+1 -1
View File
@@ -443,7 +443,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// Search.
let sr = Rect::from_min_size(pos2(rect.min.x + 8.0, head.max.y + 2.0), vec2(rect.width() - 16.0, 22.0));
let mut q = app.ui.project_search.clone();
widgets::search_field(ui, sr, &mut q, "Search project", &t);
widgets::search_field(ui, sr, &mut q, crate::i18n::tr("Search project"), &t);
app.ui.project_search = q;
app.auto.add("project.search", sr, "Search");
// Column header: Name, Label, then the visible optional columns.
+2 -2
View File
@@ -456,7 +456,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let render_b = Rect::from_min_size(pos2(top.max.x - 12.0 - bw, top.min.y + 34.0), vec2(bw, 26.0));
let stop_b = Rect::from_min_size(pos2(render_b.min.x - 8.0 - bw, render_b.min.y), vec2(bw, 26.0));
let can_render = !rendering && queue.iter().any(RenderQueueItem::is_queued) && !available.is_empty();
if widgets::text_button(ui, render_b, "Render", can_render, &t, egui::Id::new("rq-render")).clicked() && can_render {
if widgets::text_button(ui, render_b, crate::i18n::tr("Render"), can_render, &t, egui::Id::new("rq-render")).clicked() && can_render {
actions.push(("renderQueue.render", json!({"wait": false})));
}
app.auto.add("renderQueue.render", render_b, "Render");
@@ -469,7 +469,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
p.text(pos2(nb.max.x + 5.0, nb.center().y), Align2::LEFT_CENTER, tr("Notify"), Tokens::ui(11.5), t.text_dim);
app.auto.add("renderQueue.notify", nb, if notify { "Notify: on" } else { "Notify: off" });
if rendering {
if widgets::text_button(ui, stop_b, "Stop", false, &t, egui::Id::new("rq-stop")).clicked() {
if widgets::text_button(ui, stop_b, crate::i18n::tr("Stop"), false, &t, egui::Id::new("rq-stop")).clicked() {
actions.push(("renderQueue.stop", json!({})));
}
app.auto.add("renderQueue.stop", stop_b, "Stop");
+1 -1
View File
@@ -69,7 +69,7 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
// Save the open project as a user template.
let can_save = app.session.project.comps().next().is_some();
let sr = Rect::from_min_size(pos2(area.max.x - 210.0, y - 4.0), vec2(210.0, 28.0));
let save = widgets::text_button(ui, sr, "Save Current as Template…", false, &t, egui::Id::new("home-templates-save")).clicked();
let save = widgets::text_button(ui, sr, crate::i18n::tr("Save Current as Template…"), false, &t, egui::Id::new("home-templates-save")).clicked();
app.auto.add("home.templates.saveCurrent", sr, "Save Current as Template…");
if save {
if can_save {
+3 -3
View File
@@ -1386,7 +1386,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let t = app.tokens;
let ctx = ui.ctx().clone();
let Some(cid) = app.session.active_comp_id() else {
ui.painter().text(rect.center(), Align2::CENTER_CENTER, "(no composition)", Tokens::ui(12.0), t.text_faint);
ui.painter().text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("(no composition)"), Tokens::ui(12.0), t.text_faint);
// Dropping footage into an empty Timeline should create its first composition,
// just like dropping onto the Project panel's New Composition button (#649).
if let Some(crate::panels::DragPayload::Item(dragged)) = egui::DragAndDrop::payload::<crate::panels::DragPayload>(&ctx).as_deref()
@@ -1517,7 +1517,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
let sr = Rect::from_min_size(pos2(tc_rect.max.x + 14.0, top + 12.0), vec2((left_w - 420.0).clamp(120.0, 220.0), 22.0));
let mut search = app.ui.timeline.search.clone();
widgets::search_field(ui, sr, &mut search, "Search", &t);
widgets::search_field(ui, sr, &mut search, crate::i18n::tr("Search"), &t);
app.ui.timeline.search = search;
app.auto.add("timeline.search", sr, "Search");
let mut bx = rect.min.x + left_w - 10.0;
@@ -2518,7 +2518,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
RowKind::Waveform { item } => {
lp.rect_filled(left, 0.0, if ri % 2 == 0 { t.row } else { t.row_alt });
let indent = cw.name + 6.0 + 14.0 * row.depth as f32;
lp.text(pos2(indent + 10.0, cy), Align2::LEFT_CENTER, "Waveform", Tokens::ui(12.0), t.text);
lp.text(pos2(indent + 10.0, cy), Align2::LEFT_CENTER, crate::i18n::tr("Waveform"), Tokens::ui(12.0), t.text);
let wr = Rect::from_min_max(pos2(graph_x0, r.min.y), r.max);
gp.rect_filled(wr, 0.0, t.tl_bg);
app.auto.add(&format!("timeline.layer.{}.waveform", layer.id.0), wr, "Waveform");
+9 -3
View File
@@ -745,7 +745,13 @@ fn locked_pane(app: &mut EffectcraftApp, ui: &mut egui::Ui, full: Rect, bg: Colo
// Close the locked viewer.
let close = Rect::from_min_size(pos2(r.max.x - 26.0, r.min.y + 4.0), vec2(20.0, 20.0));
let resp = ui.interact(close, egui::Id::new("viewer-locked-close"), Sense::click());
p.text(close.center(), Align2::CENTER_CENTER, "\u{00d7}", Tokens::ui(14.0), if resp.hovered() { Color32::WHITE } else { Color32::from_white_alpha(170) });
p.text(
close.center(),
Align2::CENTER_CENTER,
crate::i18n::tr("\u{00d7}"),
Tokens::ui(14.0),
if resp.hovered() { Color32::WHITE } else { Color32::from_white_alpha(170) },
);
app.auto.add("viewer.locked.close", close, "Close Locked Viewer");
if resp.clicked() {
let _ = app.session.execute("view.closeLockedViewer", serde_json::json!({}));
@@ -2534,12 +2540,12 @@ fn empty_state(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let c = rect.center();
p.text(c - vec2(0.0, 40.0), Align2::CENTER_CENTER, crate::i18n::tr("Composition"), Tokens::semibold(16.0), t.text);
let b1 = Rect::from_center_size(c + vec2(0.0, 0.0), vec2(220.0, 30.0));
if widgets::text_button(ui, b1, "New Composition", true, &t, egui::Id::new("empty-newcomp")).clicked() {
if widgets::text_button(ui, b1, crate::i18n::tr("New Composition"), true, &t, egui::Id::new("empty-newcomp")).clicked() {
crate::panels::dialogs::open_new_comp(app);
}
app.auto.add("viewer.empty.newComp", b1, "New Composition");
let b2 = Rect::from_center_size(c + vec2(0.0, 40.0), vec2(220.0, 30.0));
if widgets::text_button(ui, b2, "New Composition From Footage", false, &t, egui::Id::new("empty-fromfootage")).clicked() {
if widgets::text_button(ui, b2, crate::i18n::tr("New Composition From Footage"), false, &t, egui::Id::new("empty-fromfootage")).clicked() {
let _ = crate::menus::invoke(app, &ui.ctx().clone(), "file.import", json!({}));
}
app.auto.add("viewer.empty.import", b2, "New Composition From Footage");