Merge pull request #398 from FeiLiuEM/ui-i18n

i18n: translate the interface below the menu bar (panels, dialogs, tooltips)

# Conflicts:
#	crates/ui-egui/src/lib.rs
#	docs/preferences.md
This commit is contained in:
Brandon Thomas committed 2026-10-09 10:22:32 -04:00
commit 4047ea94ea
54 files changed
+1629 -450

No files matched your search

+1
View File
@@ -1,5 +1,6 @@
# Roadmap
- 2026-10-09: Settings ▸ General ▸ Language now translates the interface below the menu bar as well: panels, dialogs, buttons and tooltips read their text through `crates/ui-egui/src/i18n/ui.rs`, keyed by the English source string. Simplified and Traditional Chinese are complete (384 strings); Japanese rows can follow in the same file. A test fails when a converted panel gains a hard-coded string, and another keeps the catalog free of unused rows.
- 2026-10-08: `effectcraft-cli render --out` resolves a relative path against the working directory, like `--project` (it used the project's folder, which doubled a path that already named it). Render Queue outputs set in a project are unchanged.
- 2026-10-08: Long-press a grouped toolbar button to choose its tools, including Horizontal/Vertical Type; releasing the hold keeps the menu open. Existing point/paragraph text creation is unchanged.
+9 -8
View File
@@ -3,6 +3,7 @@
//! build.rs from contributors/contributors.json (craftrules standards/contributors.md); nothing is
//! read at run time.
use crate::i18n::{tr, tr_args};
use std::cmp::Ordering;
use egui::RichText;
@@ -243,7 +244,7 @@ pub fn contributors_ui(ui: &mut egui::Ui, auto: &mut Registry) {
let id = egui::Id::new("credits_view");
let mut v = ui.data_mut(|d| d.get_temp::<View>(id)).unwrap_or_default();
ui.horizontal_wrapped(|ui| {
ui.label("Show");
ui.label(tr("Show"));
for m in NameMode::ALL {
let r = ui.selectable_label(v.names == m, m.label());
auto.add(&format!("about.credits.show.{}", m.id()), r.rect, m.label());
@@ -252,7 +253,7 @@ pub fn contributors_ui(ui: &mut egui::Ui, auto: &mut Registry) {
}
}
ui.separator();
ui.label("Sort");
ui.label(tr("Sort"));
let combo = egui::ComboBox::from_id_salt("credits_sort").selected_text(v.key.label().0).show_ui(ui, |ui| {
for k in SortKey::ALL {
let r = ui.selectable_label(v.key == k, k.label().0);
@@ -264,29 +265,29 @@ pub fn contributors_ui(ui: &mut egui::Ui, auto: &mut Registry) {
}
});
auto.add("about.credits.sort", combo.response.rect, "Sort");
let r = ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text("Reverse the order");
let r = ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text(tr("Reverse the order"));
auto.add("about.credits.reverse", r.rect, "Reverse the order");
if r.clicked() {
v.ascending = !v.ascending;
}
ui.separator();
let r = ui.selectable_label(!v.table, "Grab bag");
let r = ui.selectable_label(!v.table, tr("Grab bag"));
auto.add("about.credits.view.grabBag", r.rect, "Grab bag");
if r.clicked() {
v.table = false;
}
let r = ui.selectable_label(v.table, "Table");
let r = ui.selectable_label(v.table, tr("Table"));
auto.add("about.credits.view.table", r.rect, "Table");
if r.clicked() {
v.table = true;
}
});
let list = sorted(CONTRIBUTORS, v.names, v.key, v.ascending);
ui.label(RichText::new(format!("{} contributors · {} commits", list.len(), group(TOTAL_COMMITS))).small().weak());
ui.label(RichText::new(tr_args("{} contributors · {} commits", &[&(list.len()), &(group(TOTAL_COMMITS))])).small().weak());
ui.separator();
egui::ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
if list.is_empty() {
ui.label("No contributor data was built into this copy.");
ui.label(tr("No contributor data was built into this copy."));
} else if v.table {
table(ui, &list, &mut v, auto);
} else {
@@ -340,7 +341,7 @@ fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View, auto: &mut Regi
/// About ▸ Models: AI models credited in Co-Authored-By trailers.
pub fn models_ui(ui: &mut egui::Ui) {
if MODELS.is_empty() {
ui.label("No model credits were built into this copy.");
ui.label(tr("No model credits were built into this copy."));
return;
}
let assisted: u64 = MODELS.iter().map(|m| m.commits).max().unwrap_or(0).max(1);
+14 -13
View File
@@ -1,6 +1,7 @@
//! The Tools bar: home, the tool slots, tool options, snapping, workspaces and the community
//! buttons (Discord is always one click away).
use crate::i18n::tr;
use egui::{Align2, Color32, Rect, Sense, Stroke, StrokeKind, pos2, vec2};
use serde_json::json;
@@ -27,14 +28,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
paint_logo(&p, brand);
let bresp = ui.interact(brand, egui::Id::new("brand"), Sense::click());
app.auto.add("header.about", brand, "About EffectCraft");
if bresp.on_hover_text("About EffectCraft").clicked() {
if bresp.on_hover_text(tr("About EffectCraft")).clicked() {
app.dialog = Some(Dialog::About);
}
x += 32.0;
// Home.
let home = Rect::from_min_size(pos2(x, cy - 13.0), vec2(26.0, 26.0));
if widgets::icon_button(ui, home, Icon::Home, app.ui.start_screen, &t, egui::Id::new("tool-home")).on_hover_text("Home").clicked() {
if widgets::icon_button(ui, home, Icon::Home, app.ui.start_screen, &t, egui::Id::new("tool-home")).on_hover_text(tr("Home")).clicked() {
app.ui.start_screen = !app.ui.start_screen;
}
app.auto.add("header.home", home, "Home");
@@ -171,7 +172,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
app.ui.snapping = app.session.state.snapping;
}
app.auto.add("header.snapping", snap, "Snapping");
let label = p.text(pos2(snap.max.x + 4.0, cy), Align2::LEFT_CENTER, "Snapping", Tokens::ui(12.0), t.text_dim);
let label = p.text(pos2(snap.max.x + 4.0, cy), Align2::LEFT_CENTER, tr("Snapping"), Tokens::ui(12.0), t.text_dim);
snapping_options(app, ui, Rect::from_min_size(pos2(label.max.x + 2.0, cy - 9.0), vec2(18.0, 18.0)));
// Right side: community buttons, workspaces.
@@ -181,9 +182,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let dc = Color32::from_rgb(0x58, 0x65, 0xf2);
p.rect_filled(discord, 13.0, if dresp.hovered() { dc.gamma_multiply(1.2) } else { dc });
icons::paint(&p, Rect::from_center_size(pos2(discord.min.x + 16.0, cy), vec2(14.0, 14.0)), Icon::Chat, Color32::WHITE);
p.text(pos2(discord.min.x + 28.0, cy), Align2::LEFT_CENTER, "Discord", Tokens::semibold(12.0), Color32::WHITE);
p.text(pos2(discord.min.x + 28.0, cy), Align2::LEFT_CENTER, tr("Discord"), Tokens::semibold(12.0), Color32::WHITE);
app.auto.add("header.discord", discord, "Join the ArtCraft Discord");
if dresp.on_hover_text("Join the ArtCraft community on Discord").clicked() {
if dresp.on_hover_text(tr("Join the ArtCraft community on Discord")).clicked() {
let _ = app.session.execute("help.discord", json!({}));
}
rx = discord.min.x - 6.0;
@@ -256,7 +257,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
fn snapping_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, r: Rect) {
let t = app.tokens;
let pop = egui::Id::new("snap-options");
if widgets::icon_button(ui, r, Icon::ChevronDown, false, &t, egui::Id::new("snap-options-button")).on_hover_text("Snapping options").clicked() {
if widgets::icon_button(ui, r, Icon::ChevronDown, false, &t, egui::Id::new("snap-options-button")).on_hover_text(tr("Snapping options")).clicked() {
widgets::open_popup(ui, pop);
}
app.auto.add("header.snappingOptions", r, "Snapping options");
@@ -399,13 +400,13 @@ fn paint_options_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, key: &str, t
}
}
p.text(row(33.0) + vec2(134.0, 0.0), Align2::LEFT_CENTER, tp.kind.label(), Tokens::ui(11.5), t.text_dim);
p.text(row(66.0), Align2::LEFT_CENTER, "Blend Mode:", Tokens::ui(12.0), t.text_dim);
p.text(row(66.0), Align2::LEFT_CENTER, tr("Blend Mode:"), Tokens::ui(12.0), t.text_dim);
blend_rect = Rect::from_min_size(row(56.0) + vec2(84.0, 0.0), vec2(150.0, 20.0));
if widgets::dropdown(ui, blend_rect, tp.blend.label(), &t, id.with("blend-dd")).clicked() {
widgets::open_popup(ui, blend_pop);
}
app.auto.add(&format!("header.{key}Options.blend"), blend_rect, "Blend Mode");
p.text(row(98.0), Align2::LEFT_CENTER, "Opacity:", Tokens::ui(12.0), t.text_dim);
p.text(row(98.0), Align2::LEFT_CENTER, tr("Opacity:"), Tokens::ui(12.0), t.text_dim);
let (or, v, _) = widgets::hot_number_at(ui, row(89.0) + vec2(84.0, 0.0), id.with("opacity"), tp.opacity, 0.5, (0.0, 100.0), 0, "%", &t);
app.auto.add(&format!("header.{key}Options.opacity"), or, "Opacity");
if let Some(v) = v {
@@ -475,14 +476,14 @@ fn paint_swatch(p: &egui::Painter, r: Rect, paint: &ToolPaint, hovered: bool, t:
fn puppet_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, mut x: f32, cy: f32) -> f32 {
let t = app.tokens;
let o = app.session.state.puppet.clone();
p.text(pos2(x, cy), Align2::LEFT_CENTER, "Mesh:", Tokens::ui(12.0), t.text_dim);
p.text(pos2(x, cy), Align2::LEFT_CENTER, tr("Mesh:"), Tokens::ui(12.0), t.text_dim);
x += 40.0;
let show = Rect::from_min_size(pos2(x, cy - 10.0), vec2(20.0, 20.0));
if widgets::checkbox(ui, show, o.show_mesh, &t, egui::Id::new("puppet-show-mesh")).clicked() {
let _ = app.session.execute("puppet.mesh", json!({"showMesh": !o.show_mesh}));
}
app.auto.add("header.puppet.showMesh", show, "Show mesh");
p.text(pos2(show.max.x + 2.0, cy), Align2::LEFT_CENTER, "Show", Tokens::ui(12.0), t.text_dim);
p.text(pos2(show.max.x + 2.0, cy), Align2::LEFT_CENTER, tr("Show"), Tokens::ui(12.0), t.text_dim);
x = show.max.x + 44.0;
for (label, key, v, range) in [("Expansion:", "expansion", o.expansion, (-100.0, 200.0)), ("Density:", "density", o.density, (0.0, 100.0))] {
p.text(pos2(x, cy), Align2::LEFT_CENTER, label, Tokens::ui(12.0), t.text_dim);
@@ -498,7 +499,7 @@ fn puppet_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter
let label = "Record Options...";
let w = 104.0;
let r = Rect::from_min_size(pos2(x, cy - 10.0), vec2(w, 20.0));
let resp = ui.interact(r, egui::Id::new("puppet-record-options"), egui::Sense::click()).on_hover_text("⌘/Ctrl-drag a pin to record its motion");
let resp = ui.interact(r, egui::Id::new("puppet-record-options"), egui::Sense::click()).on_hover_text(tr("⌘/Ctrl-drag a pin to record its motion"));
p.rect_stroke(r, 3.0, egui::Stroke::new(1.0, if resp.hovered() { t.accent } else { t.field_border }), egui::StrokeKind::Inside);
p.text(r.center(), Align2::CENTER_CENTER, label, Tokens::ui(11.5), t.text);
app.auto.add("header.puppet.recordOptions", r, label);
@@ -513,7 +514,7 @@ fn puppet_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter
let r = Rect::from_min_size(pos2(r.max.x + 8.0, cy - 10.0), vec2(w, 20.0));
let resp = ui
.interact(r, egui::Id::new("puppet-follow"), egui::Sense::click())
.on_hover_text("Select the leader pin, then Shift-click the pins that should trail it (hair, cloth, tails)");
.on_hover_text(tr("Select the leader pin, then Shift-click the pins that should trail it (hair, cloth, tails)"));
p.rect_stroke(r, 3.0, egui::Stroke::new(1.0, if resp.hovered() { t.accent } else { t.field_border }), egui::StrokeKind::Inside);
p.text(r.center(), Align2::CENTER_CENTER, label, Tokens::ui(11.5), t.text);
app.auto.add("header.puppet.follow", r, label);
@@ -534,7 +535,7 @@ fn ws_menu(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
}
}
ui.separator();
if ui.button("Reset to Saved Layout").clicked() {
if ui.button(tr("Reset to Saved Layout")).clicked() {
let n = app.ui.workspace.clone();
app.set_workspace(&n);
ui.close();
+4
View File
@@ -4,7 +4,11 @@
//! plug-in names remain verbatim; these catalogs only translate the fixed menu tree.
use crate::EffectcraftApp;
/// Translations for the panels, tooltips and dialogs below the menu bar (see the module docs).
mod ui;
use effectcraft_engine::menus::MenuEntry;
pub(crate) use ui::{set_current, tr, tr_args};
/// Every language with a catalog, in the order Settings ▸ General ▸ Language lists them.
const CATALOG_CODES: [&str; 4] = ["ja", "zh-hans", "zh-hant", "uk"];
File diff suppressed because it is too large. Load diff
+1
View File
@@ -1579,6 +1579,7 @@ impl eframe::App for EffectcraftApp {
// The CJK fallback fonts depend on the interface language, so they follow a change of
// Settings ▸ General ▸ Language instead of only the first frame.
let language = crate::i18n::language(self);
crate::i18n::set_current(language);
if !self.styled || self.styled_language != language {
theme::install(ctx, &self.tokens, language);
self.styled_language = language;
+4 -3
View File
@@ -6,6 +6,7 @@
//! Start Capture, pressing in the viewer records the pointer path in real time while the comp
//! plays; releasing turns it into Position keys.
use crate::i18n::{tr, tr_args};
use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2};
use serde_json::json;
@@ -170,7 +171,7 @@ pub fn motion_sketch(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
label(&p, x0, y, "Smoothing:", &t);
st.sketch_smoothing = number(app, ui, pos2(xv, y), "motionSketch.smoothing", st.sketch_smoothing, 0.1, (0.0, 100.0), 1, "");
y += ROW + 2.0;
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, "Show:", Tokens::ui(12.0), t.text_dim);
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, tr("Show:"), Tokens::ui(12.0), t.text_dim);
y += ROW - 6.0;
st.sketch_wireframe = checkbox(app, ui, pos2(x0 + 8.0, y), "motionSketch.wireframe", "Wireframe", st.sketch_wireframe);
y += ROW - 4.0;
@@ -183,7 +184,7 @@ pub fn motion_sketch(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let fr = c.frame_rate;
let (s, e) = (app.session.time(), c.work_area.1);
let tc = |x| crate::panels::timecode(&app.session, c, x);
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, format!("Start: {} Duration: {}", tc(s), tc(e - s)), Tokens::ui(11.0), t.text_faint);
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, tr_args("Start: {} Duration: {}", &[&(tc(s)), &(tc(e - s))]), Tokens::ui(11.0), t.text_faint);
let _ = fr;
y += ROW;
let b = Rect::from_min_size(pos2(x0, y), vec2(120.0, 24.0));
@@ -290,7 +291,7 @@ pub fn sketch_overlay(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
ctx.data_mut(|d| d.insert_temp(capture_id(), c));
}
None => {
painter.text(area.center_top() + vec2(0.0, 18.0), Align2::CENTER_CENTER, "Motion Sketch: drag to record", Tokens::ui(12.0), Color32::WHITE);
painter.text(area.center_top() + vec2(0.0, 18.0), Align2::CENTER_CENTER, tr("Motion Sketch: drag to record"), Tokens::ui(12.0), Color32::WHITE);
}
}
}
@@ -13,6 +13,7 @@
//!
//! Everything is dispatched to the `camera.*` commands.
use crate::i18n::{tr, tr_args};
use std::sync::Arc;
use effectcraft_engine::Session;
@@ -297,7 +298,7 @@ pub fn viewer_hook(
let some = !pts.is_empty();
let many = pts.len() > 1;
let mut pick: Option<(&str, serde_json::Value)> = None;
if ui.add_enabled(some, egui::Button::new("Set Ground Plane and Origin")).clicked() {
if ui.add_enabled(some, egui::Button::new(tr("Set Ground Plane and Origin"))).clicked() {
pick = Some(("camera.setGroundPlane", with(None, false)));
}
ui.separator();
@@ -320,7 +321,7 @@ pub fn viewer_hook(
}
}
ui.separator();
if ui.add_enabled(some, egui::Button::new("Delete Selected Points")).clicked() {
if ui.add_enabled(some, egui::Button::new(tr("Delete Selected Points"))).clicked() {
pick = Some(("camera.deletePoints", with(None, false)));
}
if let Some((cmd, q)) = pick {
@@ -419,7 +420,7 @@ pub fn editor(
let status = match app.session.camera_progress() {
Some(pr) if running => pr.banner(),
_ => match &solve {
Some(sv) => format!("Method Used: {} Average Error: {:.2} pixels", sv.method_used.label(), sv.average_error),
Some(sv) => tr_args("Method Used: {} Average Error: {} pixels", &[&(sv.method_used.label()), &format!("{:.2}", sv.average_error)]),
None if ct::tracks(&params).is_some() && app.session.camera_pending.iter().any(|(_, l, u)| *l == layer.id && *u == g.uid) => {
"Waiting to solve".into()
}
+27 -26
View File
@@ -2,6 +2,7 @@
//! Basic / Advanced / 3D Renderer tabs, Cancel / OK. Every control registers an automation id
//! (`dialog.comp.<field>`).
use crate::i18n::{tr, tr_args};
use effectcraft_engine::time::{FrameRate, format_timecode_frames, parse_timecode};
use egui::{Color32, vec2};
use serde_json::json;
@@ -174,7 +175,7 @@ fn timecode_field(ui: &mut egui::Ui, buf: &mut Option<String>, secs: &mut f64, r
pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registry) -> Option<serde_json::Value> {
let mut out = None;
ui.horizontal(|ui| {
ui.label("Composition Name:");
ui.label(tr("Composition Name:"));
let r = ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(300.0));
auto.add("dialog.comp.name", r.rect, "Composition Name");
});
@@ -193,7 +194,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
match d.tab {
Tab::Basic => {
egui::Grid::new("comp-basic").num_columns(2).spacing([12.0, 9.0]).show(ui, |ui| {
ui.label("Preset:");
ui.label(tr("Preset:"));
let current = PRESETS
.iter()
.find(|(_, w, h, par, f)| *w == d.width && *h == d.height && (par - d.pixel_aspect).abs() < 1e-3 && (f - d.fps).abs() < 1e-3)
@@ -208,7 +209,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
});
auto.add("dialog.comp.preset", r.response.rect, "Preset");
ui.end_row();
ui.label("Width:");
ui.label(tr("Width:"));
ui.horizontal(|ui| {
let (ow, oh) = (d.width, d.height);
let r = ui.add(egui::DragValue::new(&mut d.width).range(4..=30000).suffix(" px"));
@@ -221,7 +222,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
}
});
ui.end_row();
ui.label("Height:");
ui.label(tr("Height:"));
let (ow, oh) = (d.width, d.height);
let r = ui.add(egui::DragValue::new(&mut d.height).range(4..=30000).suffix(" px"));
auto.add("dialog.comp.height", r.rect, "Height");
@@ -229,7 +230,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
d.width = ((d.height as f64) * ow as f64 / oh as f64).round().max(4.0) as u32;
}
ui.end_row();
ui.label("Pixel Aspect Ratio:");
ui.label(tr("Pixel Aspect Ratio:"));
ui.horizontal(|ui| {
let cur = PIXEL_ASPECTS
.iter()
@@ -245,12 +246,12 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
});
auto.add("dialog.comp.pixelAspect", r.response.rect, "Pixel Aspect Ratio");
ui.label(
egui::RichText::new(format!("Frame Aspect Ratio: {}", aspect_label(d.width as f64 * d.pixel_aspect, d.height as f64)))
egui::RichText::new(tr_args("Frame Aspect Ratio: {}", &[&(aspect_label(d.width as f64 * d.pixel_aspect, d.height as f64))]))
.color(Color32::GRAY),
);
});
ui.end_row();
ui.label("Frame Rate:");
ui.label(tr("Frame Rate:"));
ui.horizontal(|ui| {
// Any rate can be typed; the list holds the common ones.
let r = ui.add(egui::DragValue::new(&mut d.fps).range(1.0..=999.0).speed(0.01).max_decimals(3));
@@ -263,25 +264,25 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
}
});
auto.add("dialog.comp.frameRates", r.response.rect, "Frame Rate presets");
ui.label("frames per second");
ui.label(tr("frames per second"));
if rate.supports_drop_frame() {
ui.label(egui::RichText::new("Drop Frame").color(Color32::GRAY));
ui.label(egui::RichText::new(tr("Drop Frame")).color(Color32::GRAY));
}
});
ui.end_row();
ui.label("");
let mb = d.width as f64 * d.height as f64 * 4.0 / (1024.0 * 1024.0);
ui.label(egui::RichText::new(format!("{} x {}, {mb:.1}MB per 8bpc frame", d.width, d.height)).color(Color32::GRAY));
ui.label(egui::RichText::new(tr_args("{} x {}, {}MB per 8bpc frame", &[&d.width, &d.height, &format!("{mb:.1}")])).color(Color32::GRAY));
ui.end_row();
ui.label("Start Timecode:");
ui.label(tr("Start Timecode:"));
let r = ui.horizontal(|ui| timecode_field(ui, &mut d.start_tc, &mut d.start, rate, i64::MIN / 4)).inner;
auto.add("dialog.comp.startTimecode", r.rect, "Start Timecode");
ui.end_row();
ui.label("Duration:");
ui.label(tr("Duration:"));
let r = ui.horizontal(|ui| timecode_field(ui, &mut d.dur_tc, &mut d.duration, rate, 1)).inner;
auto.add("dialog.comp.duration", r.rect, "Duration");
ui.end_row();
ui.label("Background Color:");
ui.label(tr("Background Color:"));
ui.horizontal(|ui| {
let r = crate::widgets::srgb_color_button(ui, &mut d.bg);
auto.add("dialog.comp.background", r.rect, "Background Color");
@@ -292,7 +293,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
}
Tab::Advanced => {
egui::Grid::new("comp-adv").num_columns(2).spacing([12.0, 9.0]).show(ui, |ui| {
ui.label("Anchor:");
ui.label(tr("Anchor:"));
egui::Grid::new("comp-anchor").spacing([2.0, 2.0]).show(ui, |ui| {
for i in 0..9u8 {
let on = d.anchor == i;
@@ -309,28 +310,28 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
});
ui.end_row();
ui.label("");
let r = ui.checkbox(&mut d.preserve_frame_rate, "Preserve frame rate when nested or in render queue");
let r = ui.checkbox(&mut d.preserve_frame_rate, tr("Preserve frame rate when nested or in render queue"));
auto.add("dialog.comp.preserveFrameRate", r.rect, "Preserve frame rate when nested or in render queue");
ui.end_row();
ui.label("");
let r = ui.checkbox(&mut d.preserve_resolution, "Preserve resolution when nested");
let r = ui.checkbox(&mut d.preserve_resolution, tr("Preserve resolution when nested"));
auto.add("dialog.comp.preserveResolution", r.rect, "Preserve resolution when nested");
ui.end_row();
ui.label(egui::RichText::new("Motion Blur").strong());
ui.label(egui::RichText::new(tr("Motion Blur")).strong());
ui.end_row();
ui.label("Shutter Angle:");
ui.label(tr("Shutter Angle:"));
let r = ui.add(egui::DragValue::new(&mut d.shutter_angle).range(0.0..=720.0).suffix("°"));
auto.add("dialog.comp.shutterAngle", r.rect, "Shutter Angle");
ui.end_row();
ui.label("Shutter Phase:");
ui.label(tr("Shutter Phase:"));
let r = ui.add(egui::DragValue::new(&mut d.shutter_phase).range(-360.0..=360.0).suffix("°"));
auto.add("dialog.comp.shutterPhase", r.rect, "Shutter Phase");
ui.end_row();
ui.label("Samples Per Frame:");
ui.label(tr("Samples Per Frame:"));
let r = ui.add(egui::DragValue::new(&mut d.samples).range(2..=64));
auto.add("dialog.comp.samples", r.rect, "Samples Per Frame");
ui.end_row();
ui.label("Adaptive Sample Limit:");
ui.label(tr("Adaptive Sample Limit:"));
let r = ui.add(egui::DragValue::new(&mut d.adaptive_limit).range(16..=256));
auto.add("dialog.comp.adaptiveLimit", r.rect, "Adaptive Sample Limit");
ui.end_row();
@@ -338,11 +339,11 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
}
Tab::Renderer => {
ui.horizontal(|ui| {
ui.label("Renderer:");
ui.label(tr("Renderer:"));
let r =
egui::ComboBox::from_id_salt("comp-renderer").selected_text(if d.advanced_3d { "Advanced 3D" } else { "Classic 3D" }).show_ui(ui, |ui| {
ui.selectable_value(&mut d.advanced_3d, false, "Classic 3D");
ui.selectable_value(&mut d.advanced_3d, true, "Advanced 3D");
ui.selectable_value(&mut d.advanced_3d, false, tr("Classic 3D"));
ui.selectable_value(&mut d.advanced_3d, true, tr("Advanced 3D"));
});
auto.add("dialog.comp.renderer", r.response.rect, "Renderer");
});
@@ -351,12 +352,12 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
ui.add_space(14.0);
ui.horizontal(|ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let r = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent));
let r = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
auto.add("dialog.comp.ok", r.rect, "OK");
if r.clicked() {
out = Some(params(d));
}
let r = ui.button("Cancel");
let r = ui.button(tr("Cancel"));
auto.add("dialog.comp.cancel", r.rect, "Cancel");
if r.clicked() {
out = Some(serde_json::Value::Null);
@@ -4,6 +4,7 @@
//! live in `EditorState::content_fill` (`contentFill.set`); the button runs
//! `contentFill.generate` as a background job (Window ▸ Progress).
use crate::i18n::tr;
use egui::{Align2, Rect, pos2, vec2};
use serde_json::json;
@@ -25,7 +26,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let comp = app.session.active_comp_arc();
let layer = comp.as_ref().and_then(|c| app.session.state.selected_layers.first().and_then(|l| c.layer(*l)).cloned());
// Fill Target.
p.text(pos2(x0, y + 9.0), Align2::LEFT_CENTER, "Fill Target", Tokens::semibold(12.0), t.text);
p.text(pos2(x0, y + 9.0), Align2::LEFT_CENTER, tr("Fill Target"), Tokens::semibold(12.0), t.text);
y += ROW - 6.0;
let target = match &layer {
Some(l) => {
+4 -3
View File
@@ -5,6 +5,7 @@
//!
//! Automation ids: `dialog.deleteItems.delete`, `dialog.deleteItems.cancel`.
use crate::i18n::tr;
use egui::{Color32, vec2};
use serde_json::Value;
@@ -56,15 +57,15 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
);
ui.label(egui::RichText::new(text).color(t.tab_text_active));
ui.add_space(4.0);
ui.label(egui::RichText::new("You can undo this.").color(t.text_dim).size(11.0));
ui.label(egui::RichText::new(tr("You can undo this.")).color(t.text_dim).size(11.0));
ui.add_space(18.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let r = ui.add(egui::Button::new(egui::RichText::new(" Delete ").color(Color32::WHITE)).fill(t.danger));
let r = ui.add(egui::Button::new(egui::RichText::new(tr(" Delete ")).color(Color32::WHITE)).fill(t.danger));
app.auto.add("dialog.deleteItems.delete", r.rect, "Delete");
if r.clicked() {
answer = Some(true);
}
let r = ui.button("Cancel");
let r = ui.button(tr("Cancel"));
app.auto.add("dialog.deleteItems.cancel", r.rect, "Cancel");
if r.clicked() {
answer = Some(false);
+16 -15
View File
@@ -1,6 +1,7 @@
//! Modal dialogs: About (community links, Contributors and Models credits), New Composition /
//! Composition Settings, Solid Settings and the command palette (Camera/Light Settings live in `dialogs_3d`).
use crate::i18n::{tr, tr_args};
use effectcraft_engine::project::{ItemId, ItemKind, LayerId, LayerSource};
use egui::{Align2, Color32, Rect, Sense, pos2, vec2};
use serde_json::{Value, json};
@@ -232,9 +233,9 @@ fn about(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
}
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)").small().color(t.text_faint));
ui.label(egui::RichText::new(tr("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)")).small().color(t.text_faint));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let r = ui.button("Close");
let r = ui.button(tr("Close"));
app.auto.add("about.close", r.rect, "Close");
if r.clicked() {
close = true;
@@ -265,7 +266,7 @@ fn about_main(app: &mut EffectcraftApp, ui: &mut egui::Ui, t: &Tokens, cmd: &mut
t.text_dim,
);
ui.add_space(12.0);
ui.label("A clean-room, open-source compositor for motion graphics and visual effects: native on macOS, Windows and Linux, and in the browser. Part of the ArtCraft family of creative apps.");
ui.label(tr("A clean-room, open-source compositor for motion graphics and visual effects: native on macOS, Windows and Linux, and in the browser. Part of the ArtCraft family of creative apps."));
ui.add_space(14.0);
let links: [(Icon, &str, &str, &'static str); 4] = [
(Icon::Chat, "Join the ArtCraft Discord", effectcraft_engine::links::DISCORD, "help.discord"),
@@ -349,23 +350,23 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let auto = &mut app.auto;
modal(ctx, "Solid Settings", vec2(480.0, if editing.is_some() { 430.0 } else { 390.0 }), t, |ui| {
ui.horizontal(|ui| {
ui.label("Name:");
ui.label(tr("Name:"));
let r = ui.add(egui::TextEdit::singleline(&mut name).desired_width(320.0));
auto.add("dialog.solid.name", r.rect, "Name");
});
ui.add_space(8.0);
ui.label(egui::RichText::new("Size").strong());
ui.label(egui::RichText::new(tr("Size")).strong());
egui::Grid::new("solid-grid").num_columns(2).spacing([14.0, 8.0]).show(ui, |ui| {
let (ow, oh) = (size[0], size[1]);
ui.label("Width:");
ui.label(tr("Width:"));
ui.horizontal(|ui| {
let r = ui.add(egui::DragValue::new(&mut size[0]).range(1..=30000).suffix(" px"));
auto.add("dialog.solid.width", r.rect, "Width");
let r = ui.checkbox(&mut lock, format!("Lock Aspect Ratio to {}", super::comp_settings::aspect_label(ow as f64, oh as f64)));
let r = ui.checkbox(&mut lock, tr_args("Lock Aspect Ratio to {}", &[&(super::comp_settings::aspect_label(ow as f64, oh as f64))]));
auto.add("dialog.solid.lockAspect", r.rect, "Lock Aspect Ratio");
});
ui.end_row();
ui.label("Height:");
ui.label(tr("Height:"));
let r = ui.add(egui::DragValue::new(&mut size[1]).range(1..=30000).suffix(" px"));
auto.add("dialog.solid.height", r.rect, "Height");
ui.end_row();
@@ -376,7 +377,7 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
size[0] = ((size[1] as f64) * ow as f64 / oh as f64).round().max(1.0) as u32;
}
}
ui.label("Pixel Aspect Ratio:");
ui.label(tr("Pixel Aspect Ratio:"));
let cur = super::comp_settings::PIXEL_ASPECTS
.iter()
.find(|(_, v)| (v - par).abs() < 1e-3)
@@ -403,14 +404,14 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
.color(Color32::GRAY),
);
ui.add_space(4.0);
let r = ui.button("Make Comp Size");
let r = ui.button(tr("Make Comp Size"));
auto.add("dialog.solid.makeCompSize", r.rect, "Make Comp Size");
if r.clicked() {
size = [comp_size.0, comp_size.1];
}
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label("Color:");
ui.label(tr("Color:"));
let r = crate::widgets::srgb_color_button(ui, &mut color);
auto.add("dialog.solid.color", r.rect, "Color");
});
@@ -421,15 +422,15 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
.on_disabled_hover_text("Only this layer uses this solid.");
auto.add("dialog.solid.affectAll", r.rect, "Affect all layers that use this solid");
if shared && !affect_all {
ui.label(egui::RichText::new("This change will create a new solid for this layer.").color(t.text_dim).size(11.0));
ui.label(egui::RichText::new(tr("This change will create a new solid for this layer.")).color(t.text_dim).size(11.0));
}
}
ui.add_space(14.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let r = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent));
let r = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
auto.add("dialog.solid.ok", r.rect, "OK");
ok = r.clicked();
let r = ui.button("Cancel");
let r = ui.button(tr("Cancel"));
auto.add("dialog.solid.cancel", r.rect, "Cancel");
close = r.clicked();
});
@@ -493,7 +494,7 @@ fn palette(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
.take(14)
.collect();
modal(ctx, "Command Palette", vec2(560.0, 470.0), t, |ui| {
let r = ui.add(egui::TextEdit::singleline(&mut q).hint_text("Type a command or effect…").desired_width(f32::INFINITY).font(Tokens::ui(14.0)));
let r = ui.add(egui::TextEdit::singleline(&mut q).hint_text(tr("Type a command or effect…")).desired_width(f32::INFINITY).font(Tokens::ui(14.0)));
r.request_focus();
ui.add_space(8.0);
let sel = app.dialog_state.palette_sel.min(matches.len().saturating_sub(1));
+29 -28
View File
@@ -2,6 +2,7 @@
//! Settings, Layer Settings on a camera or light). OK runs `layer.newCamera` / `layer.newLight`
//! or `layer.cameraSettings` / `layer.lightSettings` with the dialog's values.
use crate::i18n::tr;
use effectcraft_engine::project::{AutoOrient, LayerSource, LightKind};
use effectcraft_engine::render::three_d::camera::{PRESETS, angle_of_view, default_aperture, focal_for_zoom, zoom_for_focal};
use egui::{Color32, vec2};
@@ -144,12 +145,12 @@ pub fn open_light(app: &mut EffectcraftApp, layer: Option<u64>) -> Result<(), St
fn buttons(ui: &mut egui::Ui, t: &Tokens, ok: &mut bool, close: &mut bool) -> (egui::Rect, egui::Rect) {
let mut r = (egui::Rect::NOTHING, egui::Rect::NOTHING);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let b = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent));
let b = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
r.0 = b.rect;
if b.clicked() {
*ok = true;
}
let c = ui.button("Cancel");
let c = ui.button(tr("Cancel"));
r.1 = c.rect;
if c.clicked() {
*close = true;
@@ -165,16 +166,16 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING);
super::dialogs::modal(ctx, "Camera Settings", vec2(520.0, 420.0), t, |ui| {
egui::Grid::new("cam-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| {
ui.label("Type");
ui.label(tr("Type"));
egui::ComboBox::from_id_salt("cam-type").selected_text(if d.two_node { "Two-Node Camera" } else { "One-Node Camera" }).show_ui(ui, |ui| {
ui.selectable_value(&mut d.two_node, true, "Two-Node Camera");
ui.selectable_value(&mut d.two_node, false, "One-Node Camera");
ui.selectable_value(&mut d.two_node, true, tr("Two-Node Camera"));
ui.selectable_value(&mut d.two_node, false, tr("One-Node Camera"));
});
ui.end_row();
ui.label("Name");
ui.label(tr("Name"));
ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(240.0));
ui.end_row();
ui.label("Preset");
ui.label(tr("Preset"));
let focal = focal_for_zoom(w, d.zoom);
let cur = PRESETS.iter().find(|(_, f)| (f - focal).abs() < 1e-6).map(|(n, _)| *n).unwrap_or("Custom");
egui::ComboBox::from_id_salt("cam-preset").selected_text(cur).show_ui(ui, |ui| {
@@ -185,7 +186,7 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
}
});
ui.end_row();
ui.label("Zoom");
ui.label(tr("Zoom"));
ui.horizontal(|ui| {
ui.add(egui::DragValue::new(&mut d.zoom).range(1.0..=100000.0).speed(1.0).suffix(" px"));
let mut aov = angle_of_view(w, d.zoom);
@@ -198,19 +199,19 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
}
});
ui.end_row();
ui.label("Depth of Field");
ui.checkbox(&mut d.dof, "Enable Depth of Field");
ui.label(tr("Depth of Field"));
ui.checkbox(&mut d.dof, tr("Enable Depth of Field"));
ui.end_row();
ui.label("Focus Distance");
ui.label(tr("Focus Distance"));
ui.horizontal(|ui| {
ui.add_enabled(!d.lock_to_zoom, egui::DragValue::new(&mut d.focus).range(0.0..=100000.0).suffix(" px"));
ui.checkbox(&mut d.lock_to_zoom, "Lock to Zoom");
ui.checkbox(&mut d.lock_to_zoom, tr("Lock to Zoom"));
});
ui.end_row();
ui.label("Aperture");
ui.label(tr("Aperture"));
ui.add(egui::DragValue::new(&mut d.aperture).range(0.0..=10000.0).speed(0.1).suffix(" px"));
ui.end_row();
ui.label("Blur Level");
ui.label(tr("Blur Level"));
ui.add(egui::DragValue::new(&mut d.blur).range(0.0..=10000.0).suffix(" %"));
ui.end_row();
});
@@ -256,33 +257,33 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING);
super::dialogs::modal(ctx, "Light Settings", vec2(480.0, 470.0), t, |ui| {
egui::Grid::new("light-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| {
ui.label("Name");
ui.label(tr("Name"));
ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(220.0));
ui.end_row();
ui.label("Light Type");
ui.label(tr("Light Type"));
egui::ComboBox::from_id_salt("light-type").selected_text(KINDS[d.kind.min(3)].label()).show_ui(ui, |ui| {
for (i, k) in KINDS.iter().enumerate() {
ui.selectable_value(&mut d.kind, i, k.label());
}
});
ui.end_row();
ui.label("Color");
ui.label(tr("Color"));
ui.color_edit_button_rgb(&mut d.color);
ui.end_row();
ui.label("Intensity");
ui.label(tr("Intensity"));
ui.add(egui::DragValue::new(&mut d.intensity).range(-10000.0..=10000.0).suffix(" %"));
ui.end_row();
let kind = KINDS[d.kind.min(3)];
if kind == LightKind::Spot {
ui.label("Cone Angle");
ui.label(tr("Cone Angle"));
ui.add(egui::DragValue::new(&mut d.cone_angle).range(0.0..=180.0).suffix("°"));
ui.end_row();
ui.label("Cone Feather");
ui.label(tr("Cone Feather"));
ui.add(egui::DragValue::new(&mut d.cone_feather).range(0.0..=100.0).suffix(" %"));
ui.end_row();
}
if kind != LightKind::Ambient {
ui.label("Falloff");
ui.label(tr("Falloff"));
egui::ComboBox::from_id_salt("light-falloff").selected_text(FALLOFFS[d.falloff.min(2)]).show_ui(ui, |ui| {
for (i, f) in FALLOFFS.iter().enumerate() {
ui.selectable_value(&mut d.falloff, i, *f);
@@ -290,26 +291,26 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
});
ui.end_row();
if d.falloff > 0 {
ui.label("Radius");
ui.label(tr("Radius"));
ui.add(egui::DragValue::new(&mut d.radius).range(0.0..=100000.0).suffix(" px"));
ui.end_row();
ui.label("Falloff Distance");
ui.label(tr("Falloff Distance"));
ui.add(egui::DragValue::new(&mut d.falloff_distance).range(0.0..=100000.0).suffix(" px"));
ui.end_row();
}
ui.label("Shadows");
ui.checkbox(&mut d.casts_shadows, "Casts Shadows");
ui.label(tr("Shadows"));
ui.checkbox(&mut d.casts_shadows, tr("Casts Shadows"));
ui.end_row();
ui.label("Shadow Darkness");
ui.label(tr("Shadow Darkness"));
ui.add_enabled(d.casts_shadows, egui::DragValue::new(&mut d.shadow_darkness).range(0.0..=100.0).suffix(" %"));
ui.end_row();
ui.label("Shadow Diffusion");
ui.label(tr("Shadow Diffusion"));
ui.add_enabled(d.casts_shadows, egui::DragValue::new(&mut d.shadow_diffusion).range(0.0..=1000.0).suffix(" px"));
ui.end_row();
}
});
ui.add_space(14.0);
ui.label(egui::RichText::new("Note: Shadows are only cast from layers with Cast Shadows enabled.").font(Tokens::ui(11.0)).color(t.text_dim));
ui.label(egui::RichText::new(tr("Note: Shadows are only cast from layers with Cast Shadows enabled.")).font(Tokens::ui(11.0)).color(t.text_dim));
ui.add_space(8.0);
rects = buttons(ui, t, &mut ok, &mut close);
});
+11 -9
View File
@@ -292,7 +292,9 @@ fn prop_row(
if matches!(prop.ui, ParamUi::Point | ParamUi::Point3) {
let cr = Rect::from_center_size(pos2(x + 8.0, cy), vec2(18.0, 18.0));
let active = app.ui.fx_pick.as_ref().is_some_and(|k| k.prop == uid && k.kind == "point");
if fw::crosshair_button(ui, cr, active, egui::Id::new(("ec-xh", uid)), &t).on_hover_text("Click, then click in the Composition panel").clicked()
if fw::crosshair_button(ui, cr, active, egui::Id::new(("ec-xh", uid)), &t)
.on_hover_text(crate::i18n::tr("Click, then click in the Composition panel"))
.clicked()
{
toggle_pick(app, "point", layer, prop);
}
@@ -320,7 +322,7 @@ fn prop_row(
let er = Rect::from_center_size(pos2(sr.max.x + 14.0, cy), vec2(18.0, 18.0));
let active = app.ui.fx_pick.as_ref().is_some_and(|k| k.prop == uid && k.kind == "color");
if fw::eyedropper_button(ui, er, active, egui::Id::new(("ec-eye", uid)), &t)
.on_hover_text("Eyedropper: click, then click in the Composition panel")
.on_hover_text(crate::i18n::tr("Eyedropper: click, then click in the Composition panel"))
.clicked()
{
toggle_pick(app, "color", layer, prop);
@@ -724,7 +726,7 @@ fn channel_popup(
auto: &str,
) -> Option<usize> {
let t = app.tokens;
p.text(pos2(x, cy), Align2::LEFT_CENTER, "Channel:", Tokens::ui(12.0), t.text_dim);
p.text(pos2(x, cy), Align2::LEFT_CENTER, crate::i18n::tr("Channel:"), Tokens::ui(12.0), t.text_dim);
let dr = Rect::from_min_size(pos2(x + 58.0, cy - 9.0), vec2(80.0, 18.0));
let pop = id.with("pop");
if widgets::dropdown(ui, dr, names.get(cur).copied().unwrap_or(""), &t, id).clicked() {
@@ -777,7 +779,7 @@ fn curves_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter,
};
let rr = Rect::from_min_size(pos2(mr.max.x + 8.0, cy - 9.0), vec2(44.0, 18.0));
let rresp = ui.interact(rr, egui::Id::new(("ec-creset", euid)), Sense::click());
p.text(rr.center(), Align2::CENTER_CENTER, "Reset", Tokens::ui(11.5), if rresp.hovered() { t.hot_text } else { t.text_dim });
p.text(rr.center(), Align2::CENTER_CENTER, crate::i18n::tr("Reset"), Tokens::ui(11.5), if rresp.hovered() { t.hot_text } else { t.text_dim });
app.auto.add(&format!("effectControls.effect.{euid}.curves.reset"), rr, "Reset curve");
if rresp.clicked() {
actions.push(("prop.set".into(), json!({"layer": layer.id.0, "prop": prop.uid, "value": fw::CurvePoints::identity().format()})));
@@ -846,7 +848,7 @@ fn levels_editor(
app.ui.fx_levels_channel.insert(euid, n);
}
} else {
p.text(pos2(x0, cy), Align2::LEFT_CENTER, "Histogram", Tokens::ui(12.0), t.text_dim);
p.text(pos2(x0, cy), Align2::LEFT_CENTER, crate::i18n::tr("Histogram"), Tokens::ui(12.0), t.text_dim);
}
let w = (r.width() - 72.0).clamp(120.0, 256.0);
let hr = Rect::from_min_size(pos2(x0 + 8.0, r.min.y + 34.0), vec2(w, 80.0));
@@ -911,7 +913,7 @@ fn levels_editor(
fn histogram_only(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, g: &PropGroup, r: Rect) {
let t = app.tokens;
let x0 = r.min.x + 30.0;
p.text(pos2(x0, r.min.y + 12.0), Align2::LEFT_CENTER, "Histogram", Tokens::ui(12.0), t.text_dim);
p.text(pos2(x0, r.min.y + 12.0), Align2::LEFT_CENTER, crate::i18n::tr("Histogram"), Tokens::ui(12.0), t.text_dim);
let w = (r.width() - 72.0).clamp(120.0, 256.0);
let hr = Rect::from_min_size(pos2(x0 + 8.0, r.min.y + 24.0), vec2(w, 80.0));
match frame_histogram(app, ui.ctx()) {
@@ -934,7 +936,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let p = ui.painter().with_clip_rect(rect);
let ctx = ui.ctx().clone();
let Some(layer) = selected_layer(app) else {
p.text(rect.center(), Align2::CENTER_CENTER, "Select a layer to see its effects", Tokens::ui(12.0), t.text_faint);
p.text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("Select a layer to see its effects"), Tokens::ui(12.0), t.text_faint);
return;
};
let Some(cid) = app.session.active_comp_id() else { return };
@@ -1000,7 +1002,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
app.auto.add(&format!("effectControls.effect.{}.name", g.uid), name, &g.name);
let reset = Rect::from_min_size(pos2(r.max.x - 96.0, r.min.y + 4.0), vec2(40.0, 18.0));
let rresp = ui.interact(reset, egui::Id::new(("ec-reset", g.uid)), Sense::click());
bp.text(reset.center(), Align2::CENTER_CENTER, "Reset", Tokens::ui(11.5), if rresp.hovered() { t.hot_text } else { t.text_dim });
bp.text(reset.center(), Align2::CENTER_CENTER, crate::i18n::tr("Reset"), Tokens::ui(11.5), if rresp.hovered() { t.hot_text } else { t.text_dim });
app.auto.add(&format!("effectControls.effect.{}.reset", g.uid), reset, "Reset");
if rresp.clicked() {
actions.push(("effect.reset".into(), json!({"layer": layer.id.0, "effect": g.uid})));
@@ -1008,7 +1010,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// About…: the effect's name and category (AE's About dialog).
let about = Rect::from_min_size(pos2(r.max.x - 50.0, r.min.y + 4.0), vec2(44.0, 18.0));
let aresp = ui.interact(about, egui::Id::new(("ec-about", g.uid)), Sense::click());
bp.text(about.center(), Align2::CENTER_CENTER, "About...", Tokens::ui(11.5), if aresp.hovered() { t.hot_text } else { t.text_dim });
bp.text(about.center(), Align2::CENTER_CENTER, crate::i18n::tr("About..."), Tokens::ui(11.5), if aresp.hovered() { t.hot_text } else { t.text_dim });
app.auto.add(&format!("effectControls.effect.{}.about", g.uid), about, "About");
let apop = egui::Id::new(("ec-about-pop", g.uid));
if aresp.clicked() {
+3 -2
View File
@@ -5,6 +5,7 @@
//! selected layers; dragging an effect onto a layer (timeline, viewer, Effect Controls) applies
//! it there.
use crate::i18n::tr;
use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2};
use serde_json::json;
@@ -276,7 +277,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// Contents menu.
let mr = Rect::from_center_size(pos2(rect.max.x - 18.0, sr.center().y), vec2(22.0, 22.0));
let mpop = egui::Id::new("fx-contents-menu");
if widgets::icon_button(ui, mr, Icon::Hamburger, false, &t, egui::Id::new("fx-menu")).on_hover_text("Effects & Presets options").clicked() {
if widgets::icon_button(ui, mr, Icon::Hamburger, false, &t, egui::Id::new("fx-menu")).on_hover_text(tr("Effects & Presets options")).clicked() {
widgets::open_popup(ui, mpop);
}
app.auto.add("effects.menu", mr, "Effects & Presets options");
@@ -394,7 +395,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
}
if y == list.min.y - scroll.offset && searching {
lp.text(pos2(list.center().x, list.min.y + 24.0), Align2::CENTER_CENTER, "No matching effects or presets", Tokens::ui(12.0), t.text_faint);
lp.text(pos2(list.center().x, list.min.y + 24.0), Align2::CENTER_CENTER, tr("No matching effects or presets"), Tokens::ui(12.0), t.text_faint);
}
let content = y + scroll.offset - list.min.y;
scroll.end(ui, &mut app.auto, "effects.scroll", content, &t);
+21 -20
View File
@@ -20,6 +20,7 @@
//! id (`essential.primary`, `essential.solo`, `essential.name`, `essential.control.<id>.value`,
//! `essential.instance.<id>.value`, `essential.export`…).
use crate::i18n::{tr, tr_args};
use effectcraft_engine::keyframe::Value as KV;
use effectcraft_engine::project::essential::{self, ControlType, EgControl, EgKind};
use effectcraft_engine::project::{ItemId, ItemKind, LayerSource, ParamUi, Property};
@@ -61,7 +62,7 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) {
let mut actions: Actions = vec![];
let mut invoke: Vec<(String, Value)> = vec![];
let Some(cid) = primary(app) else {
ui.label(RichText::new("Open a composition to build Essential Graphics").color(t.text_faint));
ui.label(RichText::new(tr("Open a composition to build Essential Graphics")).color(t.text_faint));
return (actions, invoke);
};
let project = app.session.project.clone();
@@ -69,7 +70,7 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) {
// ---- Primary + Solo.
ui.horizontal(|ui| {
ui.label(RichText::new("Primary:").color(t.text_dim));
ui.label(RichText::new(tr("Primary:")).color(t.text_dim));
let r = egui::ComboBox::from_id_salt("eg-primary").selected_text(&comp_name).width(150.0).show_ui(ui, |ui| {
for (id, _) in project.comps() {
let name = project.item(*id).map(|i| i.name.clone()).unwrap_or_default();
@@ -81,7 +82,7 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) {
app.auto.add("essential.primary", r.response.rect, &comp_name);
});
let solo = app.session.state.essential_solo;
let r = ui.add(egui::Button::new("Solo Supported Properties").selected(solo));
let r = ui.add(egui::Button::new(tr("Solo Supported Properties")).selected(solo));
app.auto.add("essential.solo", r.rect, "Solo Supported Properties");
if r.clicked() {
actions.push(("essential.soloSupported".into(), json!({"on": !solo})));
@@ -91,7 +92,7 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) {
let shown_name = if eg.name.is_empty() { comp_name.clone() } else { eg.name.clone() };
let name_id = egui::Id::new("eg-name-buf");
let mut name: String = ctx.data(|d| d.get_temp(name_id)).unwrap_or_else(|| shown_name.clone());
let r = ui.add(egui::TextEdit::singleline(&mut name).hint_text("Template name").desired_width(f32::INFINITY));
let r = ui.add(egui::TextEdit::singleline(&mut name).hint_text(tr("Template name")).desired_width(f32::INFINITY));
app.auto.add("essential.name", r.rect, &shown_name);
if r.has_focus() {
ctx.data_mut(|d| d.insert_temp(name_id, name.clone()));
@@ -107,7 +108,7 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) {
let list = ui.scope(|ui| {
ui.set_min_height(60.0);
if eg.controls.is_empty() {
ui.label(RichText::new("Drag properties here from the timeline,\nor use Animation > Add Property to Essential Graphics.").color(t.text_faint));
ui.label(RichText::new(tr("Drag properties here from the timeline,\nor use Animation > Add Property to Essential Graphics.")).color(t.text_faint));
}
controls(app, ui, cid, &eg.controls, None, &mut actions);
});
@@ -128,12 +129,12 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) {
// ---- Bottom buttons.
ui.horizontal_wrapped(|ui| {
let r = ui.menu_button("Add Formatting", |ui| {
if ui.button("Add Group").clicked() {
let r = ui.menu_button(tr("Add Formatting"), |ui| {
if ui.button(tr("Add Group")).clicked() {
actions.push(("essential.addGroup".into(), json!({"comp": cid.0})));
ui.close();
}
if ui.button("Add Comment").clicked() {
if ui.button(tr("Add Comment")).clicked() {
actions.push(("essential.addComment".into(), json!({"comp": cid.0})));
ui.close();
}
@@ -142,7 +143,7 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) {
// The selected property as a Font control (Source Text) or a uniform Scale slider.
let has_sel = !app.session.state.selected_props.is_empty();
let r = ui.add_enabled_ui(has_sel, |ui| {
ui.menu_button("Add Selected As", |ui| {
ui.menu_button(tr("Add Selected As"), |ui| {
for (label, as_type) in [("Font Control", "font"), ("Uniform Scale", "scale")] {
if ui.button(label).clicked() {
actions.push(("essential.addProperty".into(), json!({"as": as_type})));
@@ -155,14 +156,14 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) {
let footage_layer = app.session.active_comp().and_then(|c| {
app.session.state.selected_layers.iter().filter_map(|l| c.layer(*l)).find(|l| matches!(l.source, LayerSource::Footage { .. })).map(|l| l.id.0)
});
let r = ui.add_enabled(footage_layer.is_some(), egui::Button::new("Media Replacement"));
let r = ui.add_enabled(footage_layer.is_some(), egui::Button::new(tr("Media Replacement")));
app.auto.add("essential.addMedia", r.rect, "Add Media Replacement");
if r.clicked()
&& let Some(l) = footage_layer
{
actions.push(("essential.addMedia".into(), json!({"layer": l})));
}
let r = ui.add_enabled(!eg.controls.is_empty(), egui::Button::new("Export Template…"));
let r = ui.add_enabled(!eg.controls.is_empty(), egui::Button::new(tr("Export Template…")));
app.auto.add("essential.export", r.rect, "Export Template");
if r.clicked() {
invoke.push(("essential.exportTemplate".into(), json!({"comp": cid.0})));
@@ -234,7 +235,7 @@ fn controls(app: &mut EffectcraftApp, ui: &mut egui::Ui, cid: ItemId, list: &[Eg
}
}
None => {
ui.label(RichText::new("(missing)").color(t.danger));
ui.label(RichText::new(tr("(missing)")).color(t.danger));
}
}
link_menu(app, ui, cid, c, group, actions);
@@ -243,10 +244,10 @@ fn controls(app: &mut EffectcraftApp, ui: &mut egui::Ui, cid: ItemId, list: &[Eg
name_label(app, ui, cid, c, RichText::new(&c.name), actions);
let item = essential::source_media(&app.session.project, cid, c);
let name = item.and_then(|i| app.session.project.item(i)).map(|i| i.name.clone()).unwrap_or_default();
ui.label(RichText::new(format!("▣ {name}")).color(t.text_dim));
ui.label(RichText::new(tr_args("▣ {}", &[&name])).color(t.text_dim));
}
}
let r = ui.add(egui::Button::new(RichText::new("×").size(10.0)).small().frame(false)).on_hover_text("Remove");
let r = ui.add(egui::Button::new(RichText::new("×").size(10.0)).small().frame(false)).on_hover_text(tr("Remove"));
app.auto.add(&format!("essential.control.{}.remove", c.id), r.rect, "Remove");
if r.clicked() {
actions.push(("essential.remove".into(), json!({"comp": cid.0, "control": c.id})));
@@ -270,16 +271,16 @@ fn link_menu(app: &mut EffectcraftApp, ui: &mut egui::Ui, cid: ItemId, c: &EgCon
};
let has_sel = !app.session.state.selected_props.is_empty();
let r = ui.menu_button(RichText::new("⋯").size(10.0), |ui| {
if ui.button("Add Mirror").clicked() {
if ui.button(tr("Add Mirror")).clicked() {
actions.push(("essential.addMirror".into(), json!({"comp": cid.0, "control": c.id, "group": group})));
ui.close();
}
if ui.add_enabled(has_sel, egui::Button::new("Link Selected Property")).clicked() {
if ui.add_enabled(has_sel, egui::Button::new(tr("Link Selected Property"))).clicked() {
actions.push(("essential.linkProperty".into(), json!({"comp": cid.0, "control": c.id})));
ui.close();
}
for (layer, prop, name) in &links {
if ui.button(format!("Unlink {name}")).clicked() {
if ui.button(tr_args("Unlink {}", &[&name])).clicked() {
actions.push(("essential.unlinkProperty".into(), json!({"comp": cid.0, "control": c.id, "layer": layer, "prop": prop})));
ui.close();
}
@@ -460,7 +461,7 @@ fn instance(app: &mut EffectcraftApp, ui: &mut egui::Ui, actions: &mut Actions)
let over = essential::overridden(&layer);
let lt = layer.layer_time(app.session.time());
ui.separator();
ui.label(RichText::new(format!("Essential Properties — {}", layer.name)).strong());
ui.label(RichText::new(tr_args("Essential Properties — {}", &[&layer.name])).strong());
let group = essential::group(&layer).cloned().unwrap_or_else(|| effectcraft_engine::project::PropGroup::new(0, "", ""));
for c in eg.flat() {
let m = essential::match_id(c.id);
@@ -527,12 +528,12 @@ fn instance(app: &mut EffectcraftApp, ui: &mut egui::Ui, actions: &mut Actions)
}
ui.horizontal(|ui| {
let any = !over.is_empty();
let r = ui.add_enabled(any, egui::Button::new("Push All to Comp"));
let r = ui.add_enabled(any, egui::Button::new(tr("Push All to Comp")));
app.auto.add("essential.instance.pushAll", r.rect, "Push Override Values to Source");
if r.clicked() {
actions.push(("essential.pushToComp".into(), json!({"layer": layer.id.0})));
}
let r = ui.add_enabled(any, egui::Button::new("Revert All"));
let r = ui.add_enabled(any, egui::Button::new(tr("Revert All")));
app.auto.add("essential.instance.revertAll", r.rect, "Revert All");
if r.clicked() {
actions.push(("essential.revert".into(), json!({"layer": layer.id.0})));
+2 -1
View File
@@ -3,6 +3,7 @@
//! Clicking the message selects the layer and reveals the property in the timeline.
//! Automation ids: `viewer.exprErrors`, `viewer.exprErrors.prev`, `viewer.exprErrors.next`.
use crate::i18n::tr;
use effectcraft_engine::commands::expr_tools::{self, ExprError};
use effectcraft_engine::project::ItemId;
use egui::{Align2, Color32, Rect, Sense, pos2, vec2};
@@ -77,7 +78,7 @@ pub fn draw(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect, cid: ItemId
let comp_name = app.session.project.item(cid).map(|it| it.name.clone()).unwrap_or_default();
let e = c.errors[i].clone();
let msg_rect = Rect::from_min_max(pos2(rect.min.x + 26.0, rect.min.y), pos2(prev.min.x - 6.0, rect.max.y));
let resp = ui.interact(msg_rect, egui::Id::new("expr-bar-msg"), Sense::click()).on_hover_text("Click to reveal the expression in the timeline");
let resp = ui.interact(msg_rect, egui::Id::new("expr-bar-msg"), Sense::click()).on_hover_text(tr("Click to reveal the expression in the timeline"));
p.with_clip_rect(msg_rect).text(
pos2(msg_rect.min.x, rect.center().y),
Align2::LEFT_CENTER,
+3 -2
View File
@@ -6,6 +6,7 @@
//! [`build`] is pure and cycle-safe (a comp nested in itself is drawn once, with an edge back),
//! so agents get the same graph through `flowchart.graph`.
use crate::i18n::tr_args;
use std::collections::{BTreeMap, BTreeSet};
use effectcraft_engine::project::{ItemId, ItemKind, LayerSource, Project};
@@ -256,7 +257,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let bar = Rect::from_min_size(rect.min, vec2(rect.width(), 30.0));
p.rect_filled(bar, 0.0, t.panel_bg);
let root_name = app.session.project.item(rid).map(|i| i.name.clone()).unwrap_or_default();
p.text(pos2(bar.min.x + 10.0, bar.center().y), Align2::LEFT_CENTER, format!("Flowchart: {root_name}"), Tokens::medium(12.0), t.text);
p.text(pos2(bar.min.x + 10.0, bar.center().y), Align2::LEFT_CENTER, tr_args("Flowchart: {}", &[&root_name]), Tokens::medium(12.0), t.text);
let mut x = bar.max.x - 8.0;
let mut cmds: Vec<(String, Value)> = vec![];
let o = app.ui.flowchart.clone();
@@ -363,7 +364,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
_ => {}
}
}
resp.on_hover_text(format!("{} — double-click to open, {}-click to reveal", n.label, if cfg!(target_os = "macos") { "Cmd" } else { "Ctrl" }));
resp.on_hover_text(tr_args("{} — double-click to open, {}-click to reveal", &[&n.label, &(if cfg!(target_os = "macos") { "Cmd" } else { "Ctrl" })]));
}
for (id, params) in cmds {
if let Err(e) = crate::menus::invoke(app, &ctx, &id, params) {
+5 -4
View File
@@ -3,6 +3,7 @@
//! Insert Edit — that cut the marked range into the active composition at its current time.
//! Everything runs `footage.*` commands (state in `EditorState::footage_panel`).
use crate::i18n::{tr, tr_args};
use effectcraft_engine::project::{FootageKind, ItemKind};
use effectcraft_engine::time::Tick;
use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2};
@@ -18,7 +19,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.pasteboard);
let Some(view) = app.session.state.footage_panel.clone() else {
p.text(rect.center(), Align2::CENTER_CENTER, "Double-click footage in the Project panel to open it here", Tokens::ui(12.0), t.text_faint);
p.text(rect.center(), Align2::CENTER_CENTER, tr("Double-click footage in the Project panel to open it here"), Tokens::ui(12.0), t.text_faint);
app.auto.add("footage.empty", rect, "Footage panel (empty)");
return;
};
@@ -38,7 +39,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// Title strip.
let top = Rect::from_min_size(rect.min, vec2(rect.width(), 24.0));
p.rect_filled(top, 0.0, t.panel_bg);
p.text(pos2(top.min.x + 10.0, top.center().y), Align2::LEFT_CENTER, format!("Footage: {}", item.name), Tokens::semibold(12.0), t.text);
p.text(pos2(top.min.x + 10.0, top.center().y), Align2::LEFT_CENTER, tr_args("Footage: {}", &[&item.name]), Tokens::semibold(12.0), t.text);
app.auto.add("footage.title", top, &item.name);
// Bottom: ruler and controls.
// Rows, bottom up: Edit Target buttons, transport, ruler.
@@ -73,7 +74,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let c = so.color;
p.rect_filled(area.shrink(20.0), 0.0, Color32::from_rgb((c[0] * 255.0) as u8, (c[1] * 255.0) as u8, (c[2] * 255.0) as u8));
} else {
p.text(area.center(), Align2::CENTER_CENTER, "No video", Tokens::ui(12.0), t.text_faint);
p.text(area.center(), Align2::CENTER_CENTER, tr("No video"), Tokens::ui(12.0), t.text_faint);
}
// Ruler: ticks, In/Out span, CTI; click or drag to scrub.
let x_of = |tt: Tick| ruler.min.x + (tt.seconds() / dur.seconds().max(1e-9)) as f32 * ruler.width();
@@ -139,7 +140,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let ey = edit_row.min.y + 5.0;
let bx = (edit_row.max.x - 238.0).max(edit_row.min.x + 10.0);
if !target.is_empty() && bx > edit_row.min.x + 140.0 {
p.text(pos2(bx - 8.0, edit_row.center().y), Align2::RIGHT_CENTER, format!("Edit Target: {target}"), Tokens::ui(11.0), t.text_dim);
p.text(pos2(bx - 8.0, edit_row.center().y), Align2::RIGHT_CENTER, tr_args("Edit Target: {}", &[&target]), Tokens::ui(11.0), t.text_dim);
}
if kit::button(app, ui, Rect::from_min_size(pos2(bx, ey), vec2(110.0, 20.0)), "footage.rippleInsertEdit", "Ripple Insert Edit", false) && comp_ok {
kit::exec(app, "footage.rippleInsertEdit", json!({}));
+14 -13
View File
@@ -4,6 +4,7 @@
//! Keyboard Shortcuts: `shortcut_editor`). Every form runs an engine command with the collected parameters, so whatever a
//! dialog does an agent can do with one `engine.execute`.
use crate::i18n::tr;
use egui::{Color32, vec2};
use serde_json::{Map, Value, json};
@@ -761,7 +762,7 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
FieldKind::Path { value, .. } => {
ui.horizontal(|ui| {
let r = ui.add(egui::TextEdit::singleline(value).desired_width(150.0)).rect;
let b = ui.button("Browse…");
let b = ui.button(tr("Browse…"));
regs.push((format!("form.field.{}.browse", fl.key), b.rect, "Browse…".into()));
if b.clicked() {
browse = Some(i);
@@ -773,7 +774,7 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
FieldKind::SavePath(value) => {
ui.horizontal(|ui| {
let r = ui.add(egui::TextEdit::singleline(value).desired_width(150.0)).rect;
let b = ui.button("Browse…");
let b = ui.button(tr("Browse…"));
regs.push((format!("form.field.{}.browse", fl.key), b.rect, "Browse…".into()));
if b.clicked() {
browse = Some(i);
@@ -813,12 +814,12 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
});
ui.add_space(14.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let b = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent));
let b = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
regs.push(("form.ok".into(), b.rect, "OK".into()));
if b.clicked() {
ok = true;
}
let c = ui.button("Cancel");
let c = ui.button(tr("Cancel"));
regs.push(("form.cancel".into(), c.rect, "Cancel".into()));
if c.clicked() {
close = true;
@@ -872,7 +873,7 @@ pub fn show_info(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
ui.label(body);
ui.add_space(16.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent)).clicked() {
if ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)).clicked() {
close = true;
}
});
@@ -886,16 +887,16 @@ pub fn show_view_options(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Toke
let mut close = false;
super::dialogs::modal(ctx, "View Options", vec2(360.0, 330.0), t, |ui| {
let v = &mut app.ui.viewer;
ui.checkbox(&mut v.show_layer_controls, "Layer controls");
ui.checkbox(&mut v.show_masks, "Masks");
ui.checkbox(&mut v.rulers, "Rulers");
ui.checkbox(&mut v.guides, "Guides");
ui.checkbox(&mut v.grid, "Grid");
ui.checkbox(&mut v.safe_margins, "Title/action safe");
ui.checkbox(&mut v.transparency_grid, "Transparency grid");
ui.checkbox(&mut v.show_layer_controls, tr("Layer controls"));
ui.checkbox(&mut v.show_masks, tr("Masks"));
ui.checkbox(&mut v.rulers, tr("Rulers"));
ui.checkbox(&mut v.guides, tr("Guides"));
ui.checkbox(&mut v.grid, tr("Grid"));
ui.checkbox(&mut v.safe_margins, tr("Title/action safe"));
ui.checkbox(&mut v.transparency_grid, tr("Transparency grid"));
ui.add_space(12.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent)).clicked() {
if ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent)).clicked() {
close = true;
}
});
+15 -8
View File
@@ -16,6 +16,7 @@
//! Every edit is a `prop.set` engine command (undoable; one drag = one undo step) and every
//! handle registers an automation id.
use crate::i18n::{tr, tr_args};
use effectcraft_engine::effects::{ColoramaPalette, OffsetCurve};
use effectcraft_engine::geom::Mat3;
use effectcraft_engine::keyframe::Value;
@@ -110,7 +111,7 @@ fn tabs(
fn reset_link(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, r: Rect, id: &str) -> bool {
let t = app.tokens;
let resp = ui.interact(r, egui::Id::new(id), Sense::click());
p.text(r.center(), Align2::CENTER_CENTER, "Reset", Tokens::ui(11.5), if resp.hovered() { t.hot_text } else { t.text_dim });
p.text(r.center(), Align2::CENTER_CENTER, tr("Reset"), Tokens::ui(11.5), if resp.hovered() { t.hot_text } else { t.text_dim });
app.auto.add(id, r, "Reset");
resp.clicked()
}
@@ -196,7 +197,7 @@ fn rgb_curves(
if let Some(np) = edit.changed {
set_str(actions, layer, g, id, np.format(), Some(key));
}
p.text(pos2(gr.min.x, gr.max.y + 11.0), Align2::LEFT_CENTER, "Click adds a point; drag one off to remove", Tokens::ui(10.5), t.text_faint);
p.text(pos2(gr.min.x, gr.max.y + 11.0), Align2::LEFT_CENTER, tr("Click adds a point; drag one off to remove"), Tokens::ui(10.5), t.text_faint);
}
/// A hue / saturation curve's points (`x,y` in 0..1, y = ½ neutral), any count (empty = flat).
@@ -394,7 +395,7 @@ fn huesat_curves(
if reset_link(app, ui, p, Rect::from_min_size(pos2(gr.max.x - 44.0, gr.max.y + 4.0), vec2(44.0, 18.0)), &format!("{prefix}.reset")) {
set_str(actions, layer, g, id, String::new(), None);
}
p.text(pos2(gr.min.x, gr.max.y + 13.0), Align2::LEFT_CENTER, "Click adds a point (½ height = no change)", Tokens::ui(10.5), t.text_faint);
p.text(pos2(gr.min.x, gr.max.y + 13.0), Align2::LEFT_CENTER, tr("Click adds a point (½ height = no change)"), Tokens::ui(10.5), t.text_faint);
}
// ---------------------------------------------------------------------------------------------
@@ -458,7 +459,7 @@ fn colorama_wheel(
let pal = if custom { ColoramaPalette::parse(&str_value(layer, ectx, g, "palette")) } else { None }.unwrap_or_else(|| ColoramaPalette::preset(preset));
let x0 = r.min.x + 38.0;
let label = if custom { "Custom palette" } else { effectcraft_engine::effects::COLORAMA_PRESETS.get(preset as usize).copied().unwrap_or("Preset") };
p.text(pos2(x0, r.min.y + 15.0), Align2::LEFT_CENTER, format!("Output Cycle: {label}"), Tokens::ui(11.5), t.text_dim);
p.text(pos2(x0, r.min.y + 15.0), Align2::LEFT_CENTER, tr_args("Output Cycle: {}", &[&label]), Tokens::ui(11.5), t.text_dim);
let size = wheel_size(r.width());
let c = pos2(x0 + size / 2.0, r.min.y + 30.0 + size / 2.0);
let (outer, inner) = (size / 2.0 - 8.0, size / 2.0 - 26.0);
@@ -572,7 +573,13 @@ fn colorama_wheel(
widgets::open_popup(ui, pop);
}
app.auto.add(&format!("{prefix}.color"), sr, "Stop color");
p.text(pos2(sr.max.x + 8.0, sr.center().y), Align2::LEFT_CENTER, format!("Stop {} at {:.0}°", sel + 1, ph * 360.0), Tokens::ui(11.0), t.text_dim);
p.text(
pos2(sr.max.x + 8.0, sr.center().y),
Align2::LEFT_CENTER,
tr_args("Stop {} at {}°", &[&(sel + 1), &format!("{:.0}", ph * 360.0)]),
Tokens::ui(11.0),
t.text_dim,
);
let mut rgb = [col[0], col[1], col[2]];
if crate::header::color_popup(ui, pop, sr.left_bottom(), &mut rgb) {
let mut st = pal.stops.clone();
@@ -687,7 +694,7 @@ fn glow_map(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, laye
}
p.rect_stroke(strip, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Outside);
app.auto.add(&format!("{prefix}.colorMap"), strip, "Glow colour map");
p.text(pos2(strip.max.x + 8.0, strip.center().y), Align2::LEFT_CENTER, "Colour map", Tokens::ui(10.5), t.text_faint);
p.text(pos2(strip.max.x + 8.0, strip.center().y), Align2::LEFT_CENTER, tr("Colour map"), Tokens::ui(10.5), t.text_faint);
if mode != 2 {
return;
}
@@ -844,12 +851,12 @@ fn extractor_layers(
None => "Custom".to_string(),
};
// Lined up with the parameter names and value popups below (Effect Controls' row layout).
p.text(pos2(r.min.x + 37.0, r.center().y), Align2::LEFT_CENTER, "Layer", Tokens::ui(12.0), t.text);
p.text(pos2(r.min.x + 37.0, r.center().y), Align2::LEFT_CENTER, tr("Layer"), Tokens::ui(12.0), t.text);
let vx = (r.min.x + r.width() * 0.48).max(r.min.x + 161.0);
let dr = Rect::from_min_size(pos2(vx, r.center().y - 9.0), vec2((r.max.x - vx - 56.0).clamp(80.0, 200.0), 18.0));
let pop = egui::Id::new(("extractor-layers", euid));
if widgets::dropdown(ui, dr, &label, &t, egui::Id::new(("extractor-layer", euid)))
.on_hover_text("The OpenEXR layer shown in red, green, blue and alpha")
.on_hover_text(tr("The OpenEXR layer shown in red, green, blue and alpha"))
.clicked()
{
widgets::open_popup(ui, pop);
+7 -1
View File
@@ -244,7 +244,13 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte
gv += step;
}
if cs.is_empty() {
p.text(plot.center(), Align2::CENTER_CENTER, "Select animated properties to show them in the Graph Editor", Tokens::ui(12.0), t.text_faint);
p.text(
plot.center(),
Align2::CENTER_CENTER,
crate::i18n::tr("Select animated properties to show them in the Graph Editor"),
Tokens::ui(12.0),
t.text_faint,
);
}
// Show Reference Graph: the other graph type, faint, scaled to the plot.
+5 -5
View File
@@ -281,7 +281,7 @@ fn rail_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, rail:
let foot_h = 22.0 + links.len() as f32 * 30.0 + 22.0 + sib_rows * 26.0;
let mut cy = (rail.max.y - 16.0 - foot_h).max(y + 16.0);
p.line_segment([pos2(x0, cy - 10.0), pos2(x0 + w, cy - 10.0)], Stroke::new(1.0, t.separator));
p.text(pos2(x0, cy + 6.0), Align2::LEFT_CENTER, "Community", Tokens::semibold(12.0), t.text_dim);
p.text(pos2(x0, cy + 6.0), Align2::LEFT_CENTER, crate::i18n::tr("Community"), Tokens::semibold(12.0), t.text_dim);
cy += 22.0;
for (icon, label, cmd) in links {
let r = Rect::from_min_size(pos2(x0, cy), vec2(w, 26.0));
@@ -301,7 +301,7 @@ fn rail_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, rail:
}
cy += 30.0;
}
p.text(pos2(x0, cy + 8.0), Align2::LEFT_CENTER, "More ArtCraft apps", Tokens::ui(11.0), t.text_faint);
p.text(pos2(x0, cy + 8.0), Align2::LEFT_CENTER, crate::i18n::tr("More ArtCraft apps"), Tokens::ui(11.0), t.text_faint);
cy += 20.0;
let sw = (w - 6.0) / 2.0;
for (i, (name, slug)) in sib.iter().enumerate() {
@@ -328,7 +328,7 @@ fn home_page(
let t = app.tokens;
let (x0, w) = (area.min.x, area.width());
let mut y = area.min.y;
p.text(pos2(x0, y + 14.0), Align2::LEFT_CENTER, "Welcome to EffectCraft", Tokens::semibold(26.0), t.tab_text_active);
p.text(pos2(x0, y + 14.0), Align2::LEFT_CENTER, crate::i18n::tr("Welcome to EffectCraft"), Tokens::semibold(26.0), t.tab_text_active);
y += 40.0;
p.text(
pos2(x0, y + 8.0),
@@ -369,7 +369,7 @@ fn home_page(
y += 64.0 + 36.0;
// Recent: heading, filter, clear.
p.text(pos2(x0, y + 12.0), Align2::LEFT_CENTER, "Recent", Tokens::semibold(17.0), t.tab_text_active);
p.text(pos2(x0, y + 12.0), Align2::LEFT_CENTER, crate::i18n::tr("Recent"), Tokens::semibold(17.0), t.tab_text_active);
let entries = recent_entries(&app.session.prefs);
let fid = egui::Id::new("home-filter");
let mut filter: String = ctx.data(|d| d.get_temp(fid)).unwrap_or_default();
@@ -380,7 +380,7 @@ fn home_page(
if !entries.is_empty() {
let cr = Rect::from_min_size(pos2(fr.min.x - 96.0, y + 3.0), vec2(84.0, 20.0));
let resp = ui.interact(cr, egui::Id::new("home-clear-recent"), Sense::click());
p.text(cr.right_center(), Align2::RIGHT_CENTER, "Clear list", Tokens::ui(11.5), if resp.hovered() { t.text } else { t.text_faint });
p.text(cr.right_center(), Align2::RIGHT_CENTER, crate::i18n::tr("Clear list"), Tokens::ui(11.5), if resp.hovered() { t.text } else { t.text_faint });
app.auto.add("home.clearRecent", cr, "Clear Recent Projects");
if resp.clicked() {
actions.push(("file.clearRecent", json!({})));
+16 -3
View File
@@ -1,5 +1,6 @@
//! Info panel: colour and position under the pointer, plus the current selection.
use crate::i18n::{tr, tr_args};
use egui::{Align2, Color32, Rect, pos2, vec2};
use crate::theme::Tokens;
@@ -59,8 +60,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if let Some(c) = comp {
let sel: Vec<String> = app.session.state.selected_layers.iter().filter_map(|id| c.layer(*id)).map(|l| l.name.clone()).collect();
let tc = crate::panels::timecode(&app.session, &c, app.session.time());
p.text(pos2(x0, y), Align2::LEFT_CENTER, if sel.is_empty() { "No layer selected".to_string() } else { sel.join(", ") }, Tokens::semibold(12.0), t.text);
p.text(pos2(x0, y + 18.0), Align2::LEFT_CENTER, format!("Time: {tc}"), Tokens::ui(12.0), t.text_dim);
p.text(
pos2(x0, y),
Align2::LEFT_CENTER,
if sel.is_empty() { tr("No layer selected").to_string() } else { sel.join(", ") },
Tokens::semibold(12.0),
t.text,
);
p.text(pos2(x0, y + 18.0), Align2::LEFT_CENTER, tr_args("Time: {}", &[&tc]), Tokens::ui(12.0), t.text_dim);
// One layer selected: Duration, In and Out like AE's Info panel.
let mut ly = y + 36.0;
if let [id] = app.session.state.selected_layers.as_slice()
@@ -93,7 +100,13 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if app.session.prefs.composition.show_rendering_progress {
let ms = app.frames.last_ms.lock().map(|v| *v).unwrap_or(0.0);
let rq = app.session.render_job.as_ref().map(|_| " • Rendering queue…").unwrap_or("");
p.text(pos2(x0, ly), Align2::LEFT_CENTER, format!("Render: {ms:.0} ms • UI {:.0} fps{rq}", app.fps), Tokens::ui(11.5), t.text_faint);
p.text(
pos2(x0, ly),
Align2::LEFT_CENTER,
tr_args("Render: {} ms • UI {} fps{}", &[&format!("{ms:.0}"), &format!("{:.0}", app.fps), &rq]),
Tokens::ui(11.5),
t.text_faint,
);
}
if !app.session.state.selected_keys.is_empty() {
// One key: its property, time and value (as After Effects' Info panel shows them).
+14 -13
View File
@@ -1,6 +1,7 @@
//! Animation dialogs: Keyframe Velocity, Keyframe Interpolation and Time Stretch. Each one is
//! pre-filled from the engine (`keys.info`, the layer) and commits through one engine command.
use crate::i18n::tr;
use egui::{Color32, vec2};
use serde_json::{Value, json};
@@ -114,10 +115,10 @@ fn buttons(ui: &mut egui::Ui, app: &mut EffectcraftApp, t: &Tokens, prefix: &str
let (mut ok, mut cancel) = (false, false);
ui.add_space(14.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let r = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent));
let r = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
app.auto.add(&format!("{prefix}.ok"), r.rect, "OK");
ok = r.clicked();
let r = ui.button("Cancel");
let r = ui.button(tr("Cancel"));
app.auto.add(&format!("{prefix}.cancel"), r.rect, "Cancel");
cancel = r.clicked();
});
@@ -144,13 +145,13 @@ pub fn velocity(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
ui.label(egui::RichText::new(tag).color(t.text_dim));
let side = if out { "out" } else { "in" };
ui.horizontal(|ui| {
ui.label("Speed:");
ui.label(tr("Speed:"));
let v = if out { &mut d.out_speed[dim] } else { &mut d.in_speed[dim] };
drag(ui, app, &format!("dialog.velocity.{side}Speed.{dim}"), v, 1.0, -1e7..=1e7, "", enabled);
ui.label(egui::RichText::new(&d.units).color(t.text_dim));
});
ui.horizontal(|ui| {
ui.label("Influence:");
ui.label(tr("Influence:"));
let v = if out { &mut d.out_inf[dim] } else { &mut d.in_inf[dim] };
drag(ui, app, &format!("dialog.velocity.{side}Influence.{dim}"), v, 0.2, 0.1..=100.0, " %", enabled);
});
@@ -159,7 +160,7 @@ pub fn velocity(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
});
ui.add_space(8.0);
}
let r = ui.checkbox(&mut d.continuous, "Continuous (Lock Outgoing to Incoming)");
let r = ui.checkbox(&mut d.continuous, tr("Continuous (Lock Outgoing to Incoming)"));
app.auto.add("dialog.velocity.continuous", r.rect, "Continuous");
if d.continuous {
d.out_speed = d.in_speed.clone();
@@ -203,13 +204,13 @@ pub fn interpolation(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens)
let (mut ok, mut cancel) = (false, false);
super::dialogs::modal(ctx, "Keyframe Interpolation", vec2(440.0, 250.0), t, |ui| {
egui::Grid::new("interp-grid").num_columns(2).spacing([14.0, 10.0]).show(ui, |ui| {
ui.label("Temporal Interpolation:");
ui.label(tr("Temporal Interpolation:"));
combo(ui, app, "dialog.interpolation.temporal", &mut d.temporal, &TEMPORAL.map(|x| x.0), true);
ui.end_row();
ui.label("Spatial Interpolation:");
ui.label(tr("Spatial Interpolation:"));
combo(ui, app, "dialog.interpolation.spatial", &mut d.spatial, &SPATIAL.map(|x| x.0), d.has_spatial);
ui.end_row();
ui.label("Roving:");
ui.label(tr("Roving:"));
combo(ui, app, "dialog.interpolation.roving", &mut d.roving, &ROVING, d.has_spatial);
ui.end_row();
});
@@ -245,19 +246,19 @@ pub fn time_stretch(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let mut d = app.dialog_state.stretch.clone();
let (mut ok, mut cancel) = (false, false);
super::dialogs::modal(ctx, "Time Stretch", vec2(420.0, 300.0), t, |ui| {
ui.label(egui::RichText::new("Stretch").font(Tokens::medium(12.5)));
ui.label(egui::RichText::new(tr("Stretch")).font(Tokens::medium(12.5)));
egui::Grid::new("stretch-grid").num_columns(2).spacing([14.0, 8.0]).show(ui, |ui| {
ui.label("Original Duration:");
ui.label(tr("Original Duration:"));
ui.label(format!("{:.2} s", d.orig_duration));
ui.end_row();
ui.label("Stretch Factor:");
ui.label(tr("Stretch Factor:"));
let before = d.percent;
drag(ui, app, "dialog.timeStretch.percent", &mut d.percent, 0.5, -10000.0..=10000.0, " %", true);
if (before - d.percent).abs() > 1e-9 && d.orig_percent.abs() > 1e-9 {
d.duration = d.orig_duration * d.percent.abs() / d.orig_percent.abs();
}
ui.end_row();
ui.label("New Duration:");
ui.label(tr("New Duration:"));
let before = d.duration;
drag(ui, app, "dialog.timeStretch.duration", &mut d.duration, 0.05, 0.01..=100000.0, " s", true);
if (before - d.duration).abs() > 1e-9 && d.orig_duration > 1e-9 {
@@ -266,7 +267,7 @@ pub fn time_stretch(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
ui.end_row();
});
ui.add_space(10.0);
ui.label(egui::RichText::new("Hold In Place").font(Tokens::medium(12.5)));
ui.label(egui::RichText::new(tr("Hold In Place")).font(Tokens::medium(12.5)));
for (i, l) in ["Layer In-point", "Current Frame", "Layer Out-point"].into_iter().enumerate() {
let r = ui.radio_value(&mut d.hold, i, l);
app.auto.add(&format!("dialog.timeStretch.hold.{i}"), r.rect, l);
+4 -3
View File
@@ -11,6 +11,7 @@
//! overlay, the segmentation span bar and the view / Freeze buttons; the Roto Brush and Refine
//! Edge tools paint their strokes here (see [`super::roto_tool`]).
use crate::i18n::{tr, tr_args};
use effectcraft_engine::effects::paint;
use effectcraft_engine::project::{Layer, LayerId};
use effectcraft_engine::render::{EvalCtx, RenderOpts, Renderer};
@@ -67,7 +68,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let p = ui.painter().with_clip_rect(rect);
let (Some(cid), Some(layer)) = (app.session.active_comp_id(), current_layer(app)) else {
p.rect_filled(rect, 0.0, t.pasteboard);
p.text(rect.center(), Align2::CENTER_CENTER, "Double-click a layer to open it in the Layer panel", Tokens::ui(12.0), t.text_faint);
p.text(rect.center(), Align2::CENTER_CENTER, tr("Double-click a layer to open it in the Layer panel"), Tokens::ui(12.0), t.text_faint);
app.auto.add("layerPanel.empty", rect, "Layer panel (empty)");
return;
};
@@ -80,12 +81,12 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// Title strip.
let top = Rect::from_min_size(rect.min, vec2(rect.width(), 24.0));
p.rect_filled(top, 0.0, t.panel_bg);
p.text(pos2(top.min.x + 10.0, top.center().y), Align2::LEFT_CENTER, format!("Layer: {}", layer.name), Tokens::semibold(12.0), t.text);
p.text(pos2(top.min.x + 10.0, top.center().y), Align2::LEFT_CENTER, tr_args("Layer: {}", &[&layer.name]), Tokens::semibold(12.0), t.text);
app.auto.add("layerPanel.title", top, &layer.name);
// Bottom bar: View menu.
let bar = Rect::from_min_max(pos2(rect.min.x, rect.max.y - 30.0), rect.max);
p.rect_filled(bar, 0.0, t.panel_bg);
p.text(pos2(bar.min.x + 10.0, bar.center().y), Align2::LEFT_CENTER, "View:", Tokens::ui(12.0), t.text_dim);
p.text(pos2(bar.min.x + 10.0, bar.center().y), Align2::LEFT_CENTER, tr("View:"), Tokens::ui(12.0), t.text_dim);
let mut names = vec!["Masks".to_string()];
if let Some(fx) = layer.effects() {
names.extend(fx.groups().map(|g| g.name.clone()));
@@ -4,6 +4,7 @@
//! `layerStyles/<style>/<property>` property, so the viewer previews live; Cancel rolls the
//! edits made in the dialog back, OK keeps them (one undo step per control drag).
use crate::i18n::tr;
use effectcraft_engine::keyframe::Value as KV;
use effectcraft_engine::project::styles::{BLENDING, STYLES};
use effectcraft_engine::project::{Layer, LayerId, ParamUi, Property};
@@ -115,7 +116,7 @@ fn control(ui: &mut egui::Ui, p: &Property, v: &KV) -> Option<Value> {
changed.then(|| json!([rgba.r(), rgba.g(), rgba.b()]))
}
(KV::Gradient(_), _) => {
ui.label(egui::RichText::new("Edit in the Timeline").weak());
ui.label(egui::RichText::new(tr("Edit in the Timeline")).weak());
None
}
_ => {
@@ -176,7 +177,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
app.auto.add("dialog.layerStyle.page", head.rect, &sel);
ui.add_space(6.0);
let Some(g) = styles.as_ref().and_then(|s| s.sub(&sel)) else {
ui.label(egui::RichText::new("Not applied: tick it in the list to add it.").weak());
ui.label(egui::RichText::new(tr("Not applied: tick it in the list to add it.")).weak());
return;
};
egui::ScrollArea::vertical().max_height(400.0).show(ui, |ui| {
@@ -203,12 +204,12 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
});
ui.add_space(8.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let ok = ui.button("OK");
let ok = ui.button(tr("OK"));
app.auto.add("dialog.layerStyle.ok", ok.rect, "OK");
if ok.clicked() {
close = Some(true);
}
let cancel = ui.button("Cancel");
let cancel = ui.button(tr("Cancel"));
app.auto.add("dialog.layerStyle.cancel", cancel.rect, "Cancel");
if cancel.clicked() {
close = Some(false);
+17 -10
View File
@@ -2,6 +2,7 @@
//! tutorial (engine model in `effectcraft_engine::learn`). The coach highlights the step's target
//! (an automation id) and moves on when the user performs the step's command; "Show me" runs it.
use crate::i18n::{tr, tr_args};
use egui::{Align2, Color32, Rect, Sense, Stroke, StrokeKind, pos2, vec2};
use serde_json::json;
@@ -13,7 +14,7 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
let t = app.tokens;
let p = ui.painter().with_clip_rect(area);
let (x, mut y, w) = (area.min.x, area.min.y, area.width());
p.text(pos2(x, y + 6.0), Align2::LEFT_CENTER, "Learn", Tokens::semibold(16.0), Color32::WHITE);
p.text(pos2(x, y + 6.0), Align2::LEFT_CENTER, tr("Learn"), Tokens::semibold(16.0), Color32::WHITE);
p.text(
pos2(x, y + 26.0),
Align2::LEFT_CENTER,
@@ -112,11 +113,11 @@ pub fn coach(app: &mut EffectcraftApp, ctx: &egui::Context) {
.show(ui, |ui| {
ui.set_width(w - 28.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("LEARN").font(Tokens::semibold(10.0)).color(t.accent));
ui.label(egui::RichText::new(tr("LEARN")).font(Tokens::semibold(10.0)).color(t.accent));
ui.label(egui::RichText::new(&tut.title).font(Tokens::ui(11.0)).color(t.text_dim));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let (cr, r) = ui.allocate_exact_size(vec2(16.0, 16.0), Sense::click());
let r = r.on_hover_text("Close tutorial");
let r = r.on_hover_text(tr("Close tutorial"));
crate::icons::paint(ui.painter(), cr, crate::icons::Icon::Close, if r.hovered() { Color32::WHITE } else { t.text_dim });
app.auto.add("learn.close", r.rect, "Close tutorial");
if r.clicked() {
@@ -140,37 +141,43 @@ pub fn coach(app: &mut EffectcraftApp, ctx: &egui::Context) {
ui.add_space(4.0);
match &step {
Some(st) => {
ui.label(egui::RichText::new(format!("Step {} of {n}: {}", i + 1, st.title)).font(Tokens::semibold(13.0)).color(Color32::WHITE));
ui.label(
egui::RichText::new(tr_args("Step {} of {}: {}", &[&(i + 1), &n, &st.title]))
.font(Tokens::semibold(13.0))
.color(Color32::WHITE),
);
ui.add_space(2.0);
ui.label(egui::RichText::new(&st.text).font(Tokens::ui(12.0)).color(t.text));
}
None => {
ui.label(egui::RichText::new("Tutorial complete").font(Tokens::semibold(13.0)).color(Color32::WHITE));
ui.label(egui::RichText::new(tr("Tutorial complete")).font(Tokens::semibold(13.0)).color(Color32::WHITE));
ui.label(
egui::RichText::new("Nice work. Pick another tutorial on the Home screen's Learn tab.").font(Tokens::ui(12.0)).color(t.text),
egui::RichText::new(tr("Nice work. Pick another tutorial on the Home screen's Learn tab."))
.font(Tokens::ui(12.0))
.color(t.text),
);
}
}
ui.add_space(8.0);
ui.horizontal(|ui| {
let back = ui.add_enabled(i > 0, egui::Button::new("Back"));
let back = ui.add_enabled(i > 0, egui::Button::new(tr("Back")));
app.auto.add("learn.back", back.rect, "Back");
if back.clicked() {
action = Some("back");
}
if step.is_some() {
let show = ui.add(egui::Button::new("Show me"));
let show = ui.add(egui::Button::new(tr("Show me")));
app.auto.add("learn.showMe", show.rect, "Show me");
if show.clicked() {
action = Some("showMe");
}
let next = ui.add(egui::Button::new("Next"));
let next = ui.add(egui::Button::new(tr("Next")));
app.auto.add("learn.next", next.rect, "Next");
if next.clicked() {
action = Some("next");
}
} else {
let more = ui.add(egui::Button::new("More tutorials"));
let more = ui.add(egui::Button::new(tr("More tutorials")));
app.auto.add("learn.more", more.rect, "More tutorials");
if more.clicked() {
action = Some("more");
+19 -18
View File
@@ -8,6 +8,7 @@
//! composition marker bin, at the right end of the time ruler, adds a composition marker where it
//! is dropped. Every change is an engine command, so it is undoable and agent-drivable.
use crate::i18n::tr;
use effectcraft_engine::project::{Comp, Layer, Marker};
use effectcraft_engine::time::Tick;
use egui::{Align2, Color32, Pos2, Rect, Sense, Stroke, pos2, vec2};
@@ -126,46 +127,46 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let title = d.title.clone();
super::dialogs::modal(ctx, &title, vec2(520.0, 560.0), t, |ui| {
egui::Grid::new("marker-grid").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
ui.label("Time:");
ui.label(tr("Time:"));
let r = ui.add(egui::DragValue::new(&mut d.time).speed(0.01).range(0.0..=1e6).suffix(" s").max_decimals(3));
app.auto.add("dialog.marker.time", r.rect, "Time");
ui.end_row();
ui.label("Duration:");
ui.label(tr("Duration:"));
let r = ui.add(egui::DragValue::new(&mut d.duration).speed(0.01).range(0.0..=1e6).suffix(" s").max_decimals(3));
app.auto.add("dialog.marker.duration", r.rect, "Duration");
ui.end_row();
ui.label("Comment:");
ui.label(tr("Comment:"));
let r = ui.add(egui::TextEdit::multiline(&mut d.comment).desired_rows(3).desired_width(320.0));
app.auto.add("dialog.marker.comment", r.rect, "Comment");
ui.end_row();
ui.label("Chapter:");
ui.label(tr("Chapter:"));
text_field(ui, app, "dialog.marker.chapter", &mut d.chapter, 320.0);
ui.end_row();
ui.label("URL:");
ui.label(tr("URL:"));
text_field(ui, app, "dialog.marker.url", &mut d.url, 320.0);
ui.end_row();
ui.label("Frame Target:");
ui.label(tr("Frame Target:"));
text_field(ui, app, "dialog.marker.frameTarget", &mut d.frame_target, 320.0);
ui.end_row();
});
ui.add_space(8.0);
let r = ui.checkbox(&mut d.cue, "Cue Point");
let r = ui.checkbox(&mut d.cue, tr("Cue Point"));
app.auto.add("dialog.marker.cue", r.rect, "Cue Point");
ui.add_enabled_ui(d.cue, |ui| {
ui.horizontal(|ui| {
let r = ui.radio_value(&mut d.cue_navigation, false, "Event");
let r = ui.radio_value(&mut d.cue_navigation, false, tr("Event"));
app.auto.add("dialog.marker.cue.event", r.rect, "Event");
let r = ui.radio_value(&mut d.cue_navigation, true, "Navigation");
let r = ui.radio_value(&mut d.cue_navigation, true, tr("Navigation"));
app.auto.add("dialog.marker.cue.navigation", r.rect, "Navigation");
ui.label("Name:");
ui.label(tr("Name:"));
text_field(ui, app, "dialog.marker.cue.name", &mut d.cue_name, 160.0);
});
let mut remove = None;
for (i, (k, v)) in d.cue_params.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.label("Parameter Name:");
ui.label(tr("Parameter Name:"));
text_field(ui, app, &format!("dialog.marker.cue.param.{i}.name"), k, 110.0);
ui.label("Value:");
ui.label(tr("Value:"));
text_field(ui, app, &format!("dialog.marker.cue.param.{i}.value"), v, 110.0);
let r = ui.small_button("−");
app.auto.add(&format!("dialog.marker.cue.param.{i}.remove"), r.rect, "Remove");
@@ -185,10 +186,10 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
});
ui.add_space(8.0);
ui.horizontal(|ui| {
let r = ui.checkbox(&mut d.protected, "Protected Region");
let r = ui.checkbox(&mut d.protected, tr("Protected Region"));
app.auto.add("dialog.marker.protected", r.rect, "Protected Region");
ui.add_space(20.0);
ui.label("Label:");
ui.label(tr("Label:"));
let labels = effectcraft_engine::color::Label::ALL;
let cur = labels.get(d.label).copied().unwrap_or_default();
let r = egui::ComboBox::from_id_salt("dialog.marker.label").selected_text(app.session.prefs.label_name(cur)).show_ui(ui, |ui| {
@@ -202,14 +203,14 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
});
ui.add_space(14.0);
ui.horizontal(|ui| {
let r = ui.button("Delete");
let r = ui.button(tr("Delete"));
app.auto.add("dialog.marker.delete", r.rect, "Delete");
delete = r.clicked();
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let r = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(app.tokens.accent));
let r = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(app.tokens.accent));
app.auto.add("dialog.marker.ok", r.rect, "OK");
ok = r.clicked();
let r = ui.button("Cancel");
let r = ui.button(tr("Cancel"));
app.auto.add("dialog.marker.cancel", r.rect, "Cancel");
cancel = r.clicked();
});
@@ -383,7 +384,7 @@ pub(crate) fn comp_markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &C
/// one undo step. Dropped back on the bin or outside the time graph's width, nothing is added.
pub(crate) fn marker_bin(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &Comp, tm: TMap, bin: Rect, strip: Rect) {
let t = app.tokens;
let resp = ui.interact(bin, egui::Id::new("marker-bin"), Sense::drag()).on_hover_text("Composition marker bin: drag a marker to the time ruler");
let resp = ui.interact(bin, egui::Id::new("marker-bin"), Sense::drag()).on_hover_text(tr("Composition marker bin: drag a marker to the time ruler"));
app.auto.add("timeline.markerBin", bin, "Composition marker bin");
let hot = resp.hovered() || resp.dragged();
if hot {
+11 -10
View File
@@ -10,6 +10,7 @@
//! 3D Camera Tracker / Roto Brush analyses, Content-Aware Fill, Scene Edit Detection) with a
//! progress bar and a cancel button, and the recently finished ones.
use crate::i18n::tr;
use effectcraft_engine::media_browser as mb;
use effectcraft_engine::project::ItemId;
use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2};
@@ -66,7 +67,7 @@ pub fn media_browser(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let p = ui.painter().with_clip_rect(rect);
p.rect_filled(rect, 0.0, t.panel_bg);
let Some(b) = app.session.media_browser() else {
p.text(rect.center(), Align2::CENTER_CENTER, "The Media Browser is not available here (use File ▸ Import)", Tokens::ui(12.0), t.text_faint);
p.text(rect.center(), Align2::CENTER_CENTER, tr("The Media Browser is not available here (use File ▸ Import)"), Tokens::ui(12.0), t.text_faint);
app.auto.add("mediaBrowser.unavailable", rect, "Media Browser unavailable");
return;
};
@@ -126,7 +127,7 @@ pub fn media_browser(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// Left column: favourites.
let side = Rect::from_min_max(pos2(rect.min.x, rect.min.y + 54.0), pos2(rect.min.x + 160.0_f32.min(rect.width() * 0.3), rect.max.y));
p.line_segment([side.right_top(), side.right_bottom()], Stroke::new(1.0, t.separator));
p.text(pos2(side.min.x + 10.0, side.min.y + 10.0), Align2::LEFT_CENTER, "Favorites", Tokens::semibold(11.5), t.text_dim);
p.text(pos2(side.min.x + 10.0, side.min.y + 10.0), Align2::LEFT_CENTER, tr("Favorites"), Tokens::semibold(11.5), t.text_dim);
let mut fy = side.min.y + 22.0;
let mut places: Vec<(String, String)> = b.places();
places.extend(
@@ -208,22 +209,22 @@ pub fn media_browser(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
egui::DragAndDrop::set_payload(&ctx, DragPayload::Files(vec![e.path.clone()]));
}
resp.context_menu(|ui| {
if !e.is_dir && ui.button("Import").clicked() {
if !e.is_dir && ui.button(tr("Import")).clicked() {
actions.push(("mediaBrowser.import", json!({"paths": [e.path]})));
ui.close();
}
if !e.is_dir && ui.button("Import and Add to Composition").clicked() {
if !e.is_dir && ui.button(tr("Import and Add to Composition")).clicked() {
actions.push(("mediaBrowser.import", json!({"paths": [e.path], "addToComp": true})));
ui.close();
}
if e.is_dir && ui.button("Add to Favorites").clicked() {
if e.is_dir && ui.button(tr("Add to Favorites")).clicked() {
actions.push(("mediaBrowser.addFavorite", json!({"path": e.path})));
ui.close();
}
});
}
if entries.is_empty() {
lp.text(list.center(), Align2::CENTER_CENTER, "Empty folder", Tokens::ui(12.0), t.text_faint);
lp.text(list.center(), Align2::CENTER_CENTER, tr("Empty folder"), Tokens::ui(12.0), t.text_faint);
}
scroll.end(ui, &mut app.auto, "mediaBrowser.scroll", entries.len().div_ceil(cols) as f32 * tile.y + 8.0, &t);
if budget == 0 {
@@ -256,7 +257,7 @@ fn comment_field(ui: &mut egui::Ui, r: Rect, id: &str, text: &str) -> Option<Str
let key = egui::Id::new(("meta-comment", id));
let mut buf: String = ui.data(|d| d.get_temp(key)).unwrap_or_else(|| text.to_string());
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(r));
let resp = child.add(egui::TextEdit::multiline(&mut buf).desired_width(r.width()).desired_rows(2).hint_text("Add a comment"));
let resp = child.add(egui::TextEdit::multiline(&mut buf).desired_width(r.width()).desired_rows(2).hint_text(tr("Add a comment")));
if resp.has_focus() || resp.changed() {
ui.data_mut(|d| d.insert_temp(key, buf.clone()));
} else {
@@ -299,7 +300,7 @@ pub fn metadata(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
y += 10.0;
} else {
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, "Select an item in the Project panel", Tokens::ui(12.0), t.text_faint);
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, tr("Select an item in the Project panel"), Tokens::ui(12.0), t.text_faint);
y += 28.0;
}
let proj = v["project"].clone();
@@ -332,7 +333,7 @@ pub fn progress(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let x0 = rect.min.x + 12.0;
let w = (rect.width() - 24.0).max(60.0);
if jobs.is_empty() {
p.text(pos2(x0, y + 10.0), Align2::LEFT_CENTER, "No background jobs running", Tokens::ui(12.0), t.text_faint);
p.text(pos2(x0, y + 10.0), Align2::LEFT_CENTER, tr("No background jobs running"), Tokens::ui(12.0), t.text_faint);
y += 28.0;
} else {
ui.ctx().request_repaint_after(std::time::Duration::from_millis(150));
@@ -354,7 +355,7 @@ pub fn progress(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if !app.session.job_log.is_empty() {
p.line_segment([pos2(x0, y), pos2(x0 + w, y)], Stroke::new(1.0, t.separator));
y += 6.0;
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, "Finished", Tokens::semibold(11.5), t.text_dim);
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, tr("Finished"), Tokens::semibold(11.5), t.text_dim);
y += 20.0;
for r in app.session.job_log.iter().rev().take(20) {
let col = match r.status.as_str() {
+6 -5
View File
@@ -1,5 +1,6 @@
//! Smaller panels: Preview, Audio, History, Markers and placeholders.
use crate::i18n::{tr, tr_args};
use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2};
use serde_json::json;
@@ -10,7 +11,7 @@ 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, format!("{} — coming soon", p.title()), Tokens::ui(12.0), t.text_faint);
ui.painter().text(rect.center(), Align2::CENTER_CENTER, tr_args("{} — coming soon", &[&(p.title())]), Tokens::ui(12.0), t.text_faint);
}
/// Preview panel: transport controls, the shortcut whose options are shown, and that
@@ -158,7 +159,7 @@ pub fn preview(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
changes.push(json!({"shortcut": current.id(), "frameRate": v}));
}
let sx = xv + 106.0;
p.text(pos2(sx, yy), Align2::LEFT_CENTER, "Skip", Tokens::ui(12.0), t.text_dim);
p.text(pos2(sx, yy), Align2::LEFT_CENTER, tr("Skip"), Tokens::ui(12.0), t.text_dim);
let (r, nv, _) = widgets::hot_number_at(ui, pos2(sx + 30.0, yy - 9.0), egui::Id::new("pv-skip"), o.skip as f64, 0.1, (0.0, 99.0), 0, "", &t);
app.auto.add("preview.skip", r, "Skip");
if let Some(nv) = nv {
@@ -194,7 +195,7 @@ pub fn preview(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
yy += 24.0;
// What stopping does.
p.text(pos2(x0, yy), Align2::LEFT_CENTER, format!("On ({}) stop:", current.label()), Tokens::ui(11.5), t.text_dim);
p.text(pos2(x0, yy), Align2::LEFT_CENTER, tr_args("On ({}) stop:", &[&(current.label())]), Tokens::ui(11.5), t.text_dim);
yy += 20.0;
if check_row(app, ui, &p, pos2(x0 - 2.0, yy), "playCachedFrames", "If caching, play cached frames", o.play_cached_frames) {
changes.push(json!({"shortcut": current.id(), "playCachedFrames": !o.play_cached_frames}));
@@ -353,7 +354,7 @@ pub fn audio(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
}
None => {
p.text(pos2(lx, top + 12.0), Align2::LEFT_CENTER, "No audio layer selected", Tokens::ui(11.0), t.text_faint);
p.text(pos2(lx, top + 12.0), Align2::LEFT_CENTER, tr("No audio layer selected"), Tokens::ui(11.0), t.text_faint);
}
}
if app.meter.active() || app.audio.is_some() {
@@ -425,7 +426,7 @@ pub fn markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let scroll = widgets::PanelScroll::begin(ui, egui::Id::new("markers-scroll"), rect);
let mut y = rect.min.y + 8.0 - scroll.offset;
if c.markers.is_empty() {
p.text(rect.center(), Align2::CENTER_CENTER, "No composition markers", Tokens::ui(12.0), t.text_faint);
p.text(rect.center(), Align2::CENTER_CENTER, tr("No composition markers"), Tokens::ui(12.0), t.text_faint);
}
for m in &c.markers {
let r = Rect::from_min_size(pos2(rect.min.x, y), vec2(rect.width(), 22.0));
+5 -4
View File
@@ -56,6 +56,7 @@ pub mod viewer_tools;
pub mod viewers;
pub mod waveform;
use crate::i18n::tr;
use effectcraft_engine::Session;
use effectcraft_engine::project::Comp;
use effectcraft_engine::time::Tick;
@@ -138,17 +139,17 @@ pub fn panel_menu_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
let area = egui::Area::new(id.with("area")).order(egui::Order::Foreground).fixed_pos(pos).show(ui.ctx(), |ui| {
egui::Frame::popup(ui.style()).show(ui, |ui| {
ui.set_min_width(200.0);
if ui.button("Close Panel").clicked() {
if ui.button(tr("Close Panel")).clicked() {
app.close_panel(panel);
close = true;
}
let floating = app.ui.floating.iter().any(|f| f.panels.contains(&panel));
if !floating && ui.button("Undock Panel").clicked() {
if !floating && ui.button(tr("Undock Panel")).clicked() {
let r = crate::dock_ui::default_float_rect(ui.ctx().content_rect());
app.edit_layout(|l| l.float(panel, r));
close = true;
}
if !floating && ui.button(if app.ui.maximized == Some(panel) { "Restore Panel Size" } else { "Maximize Panel" }).clicked() {
if !floating && ui.button(if app.ui.maximized == Some(panel) { tr("Restore Panel Size") } else { tr("Maximize Panel") }).clicked() {
app.toggle_maximize(panel);
close = true;
}
@@ -166,7 +167,7 @@ pub fn panel_menu_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
close = true;
}
}
if ui.button("Composition Settings…").clicked() {
if ui.button(tr("Composition Settings…")).clicked() {
let _ = dialogs::open_comp_settings(app);
close = true;
}
+9 -8
View File
@@ -3,6 +3,7 @@
//! Both edit the session's paint options through `paint.options` / `paint.brushPreset`, so
//! agents see and set exactly what the panels show.
use crate::i18n::tr;
use effectcraft_engine::effects::paint::{BRUSH_PRESETS, CHANNELS, DURATIONS, ERASE_MODES, MODES};
use egui::{Align2, Color32, Rect, Sense, Stroke, StrokeKind, pos2, vec2};
use serde_json::{Value, json};
@@ -112,7 +113,7 @@ pub fn paint(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let sresp = ui.interact(swap, egui::Id::new("paint-swap"), Sense::click());
p.text(swap.center(), Align2::CENTER_CENTER, "⇄", Tokens::ui(12.0), if sresp.hovered() { t.text } else { t.text_dim });
app.auto.add("paint.swapColors", swap, "Swap colors");
if sresp.on_hover_text("Swap Paint Colors (X)").clicked() {
if sresp.on_hover_text(tr("Swap Paint Colors (X)")).clicked() {
set(app, json!({"color": o.background, "background": o.color}));
}
let reset = Rect::from_min_size(pos2(x0 + 46.0, y + 18.0), vec2(18.0, 14.0));
@@ -120,7 +121,7 @@ pub fn paint(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
p.rect_filled(Rect::from_min_size(reset.min + vec2(4.0, 3.0), vec2(7.0, 6.0)), 0.0, Color32::WHITE);
p.rect_stroke(Rect::from_min_size(reset.min + vec2(8.0, 6.0), vec2(7.0, 6.0)), 0.0, Stroke::new(1.0, t.text_dim), StrokeKind::Inside);
app.auto.add("paint.resetColors", reset, "Default colors");
if rresp.on_hover_text("Set Paint Colors to Black and White (D)").clicked() {
if rresp.on_hover_text(tr("Set Paint Colors to Black and White (D)")).clicked() {
set(app, json!({"color": [1.0, 1.0, 1.0, 1.0], "background": [0.0, 0.0, 0.0, 1.0]}));
}
y += 40.0;
@@ -168,9 +169,9 @@ pub fn paint(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// Clone Options.
p.line_segment([pos2(x0, y), pos2(x0 + w, y)], Stroke::new(1.0, t.separator));
y += 8.0;
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, "Clone Options", Tokens::semibold(11.5), if clone { t.text } else { t.text_faint });
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, tr("Clone Options"), Tokens::semibold(11.5), if clone { t.text } else { t.text_faint });
y += 22.0;
p.text(pos2(x0, y + 10.0), Align2::LEFT_CENTER, "Preset:", Tokens::ui(11.5), t.text_dim);
p.text(pos2(x0, y + 10.0), Align2::LEFT_CENTER, tr("Preset:"), Tokens::ui(11.5), t.text_dim);
for i in 1..=5u32 {
let r = Rect::from_min_size(pos2(x0 + 56.0 + (i - 1) as f32 * 26.0, y), vec2(22.0, 20.0));
let resp = ui.interact(r, egui::Id::new(("clone-preset", i)), Sense::click());
@@ -217,7 +218,7 @@ pub fn paint(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
y += 26.0;
let off = o.clone_offset.unwrap_or([0.0, 0.0]);
p.text(pos2(x0, y + 9.0), Align2::LEFT_CENTER, "Offset:", Tokens::ui(11.5), t.text_dim);
p.text(pos2(x0, y + 9.0), Align2::LEFT_CENTER, tr("Offset:"), Tokens::ui(11.5), t.text_dim);
for (d, v) in off.iter().enumerate() {
let (r, nv, _) = widgets::hot_number_at(ui, pos2(x0 + 56.0 + d as f32 * 64.0, y), egui::Id::new(("clone-off", d)), *v, 1.0, (-1e5, 1e5), 0, "", &t);
app.auto.add(&format!("paint.cloneOffset.{d}"), r, "Clone offset");
@@ -240,7 +241,7 @@ pub fn paint(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
set(app, json!({"showOverlay": !o.show_overlay}));
}
app.auto.add("paint.showOverlay", r, "Clone Source Overlay");
p.text(pos2(r.max.x + 4.0, r.center().y), Align2::LEFT_CENTER, "Clone Source Overlay:", Tokens::ui(11.5), t.text_dim);
p.text(pos2(r.max.x + 4.0, r.center().y), Align2::LEFT_CENTER, tr("Clone Source Overlay:"), Tokens::ui(11.5), t.text_dim);
if let Some(nv) = number(app, ui, &p, x0 + 150.0, y, "", "paint.overlayOpacity", o.overlay_opacity, (0.0, 100.0), " %", true) {
set(app, json!({"overlayOpacity": nv}));
}
@@ -329,7 +330,7 @@ pub fn brushes(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
let r = Rect::from_min_size(pos2(x0, y), vec2(18.0, 20.0));
// Spacing (checkbox in AE turns spacing on; we always space dabs, so it shows the value).
p.text(pos2(x0, y + 9.0), Align2::LEFT_CENTER, "Spacing:", Tokens::ui(11.5), t.text_dim);
p.text(pos2(x0, y + 9.0), Align2::LEFT_CENTER, tr("Spacing:"), Tokens::ui(11.5), t.text_dim);
let _ = r;
let (sr, nv, _) = widgets::hot_number_at(ui, pos2(x0 + 58.0, y), egui::Id::new("brushes-spacing"), o.spacing, 0.5, (1.0, 1000.0), 0, " %", &t);
app.auto.add("brushes.spacing", sr, "Spacing");
@@ -338,7 +339,7 @@ pub fn brushes(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
y += 30.0;
// Brush Dynamics.
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, "Brush Dynamics", Tokens::semibold(11.5), t.text);
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, tr("Brush Dynamics"), Tokens::semibold(11.5), t.text);
y += 22.0;
let opts = vec!["Off".to_string(), "Pen Pressure".to_string()];
for (label, key, on) in
+9 -8
View File
@@ -12,6 +12,7 @@
//! `createNulls.tracePath`, `createNulls.loop`, `vrEditor.env.<output id>`, `vrEditor.pan`,
//! `vrEditor.tilt`, `vrEditor.roll`, `vrEditor.reset`, `vrEditor.open`.
use crate::i18n::tr;
use egui::{Rect, RichText};
use serde_json::{Value, json};
@@ -38,7 +39,7 @@ pub fn create_nulls(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let loop_id = egui::Id::new("createNulls.loop");
let mut looping: bool = ui.data(|d| d.get_temp(loop_id)).unwrap_or(false);
let ready = app.session.is_enabled("paths.nullsFollowPoints") && !app.session.state.selected_layers.is_empty();
ui.label(RichText::new("Select a layer with a mask or shape path (or the path property), or Puppet pins.").color(t.text_dim).small());
ui.label(RichText::new(tr("Select a layer with a mask or shape path (or the path property), or Puppet pins.")).color(t.text_dim).small());
ui.add_space(6.0);
let mut acts = vec![];
for (id, label, tip) in [
@@ -53,7 +54,7 @@ pub fn create_nulls(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
acts.push((id, if id == "paths.tracePath" { json!({"loop": looping}) } else { json!({}) }));
}
}
let r = ui.checkbox(&mut looping, "Loop (Trace Path)");
let r = ui.checkbox(&mut looping, tr("Loop (Trace Path)"));
app.auto.add("createNulls.loop", r.rect, "Loop");
ui.data_mut(|d| d.insert_temp(loop_id, looping));
run(app, &ctx, acts);
@@ -66,7 +67,7 @@ pub fn vr_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let mut ui = area(ui, rect);
let envs = app.session.execute("comp.vr.environments", json!({})).ok().and_then(|v| v.as_array().cloned()).unwrap_or_default();
if envs.is_empty() {
ui.label(RichText::new("No VR environments yet. Create one with Composition ▸ VR ▸ Create VR Environment…").color(t.text_faint));
ui.label(RichText::new(tr("No VR environments yet. Create one with Composition ▸ VR ▸ Create VR Environment…")).color(t.text_faint));
return;
}
let sel_id = egui::Id::new("vrEditor.selected");
@@ -78,7 +79,7 @@ pub fn vr_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
sel = active.and_then(|c| envs.iter().find(|e| owns(e, c))).or(envs.first()).and_then(|e| e["output"].as_u64()).unwrap_or(0);
}
let mut acts = vec![];
ui.label(RichText::new("VR Environments").strong());
ui.label(RichText::new(tr("VR Environments")).strong());
for e in &envs {
let out = e["output"].as_u64().unwrap_or(0);
let r = ui.selectable_label(out == sel, e["name"].as_str().unwrap_or(""));
@@ -90,7 +91,7 @@ pub fn vr_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
ui.data_mut(|d| d.insert_temp(sel_id, sel));
let Some(env) = envs.iter().find(|e| e["output"] == sel) else { return };
ui.separator();
ui.label(RichText::new("360 View Orientation").strong());
ui.label(RichText::new(tr("360 View Orientation")).strong());
let view: Vec<f64> = env["view"].as_array().map(|a| a.iter().filter_map(Value::as_f64).collect()).unwrap_or_default();
let signed = |a: f64| if a > 180.0 { a - 360.0 } else { a };
let (mut tilt, mut pan, mut roll) =
@@ -108,17 +109,17 @@ pub fn vr_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
acts.push(("comp.vr.setView", json!({"comp": sel, "pan": pan, "tilt": tilt, "roll": roll})));
}
ui.horizontal(|ui| {
let r = ui.button("Reset");
let r = ui.button(tr("Reset"));
app.auto.add("vrEditor.reset", r.rect, "Reset");
if r.clicked() {
acts.push(("comp.vr.setView", json!({"comp": sel, "orientation": [0.0, 0.0, 0.0]})));
}
let r = ui.button("Open Output");
let r = ui.button(tr("Open Output"));
app.auto.add("vrEditor.open", r.rect, "Open Output");
if r.clicked() {
acts.push(("comp.open", json!({"comp": sel})));
}
});
ui.label(RichText::new("All six face cameras turn together; the scene is not changed.").color(t.text_faint).small());
ui.label(RichText::new(tr("All six face cameras turn together; the scene is not changed.")).color(t.text_faint).small());
run(app, &ctx, acts);
}
+10 -9
View File
@@ -2,6 +2,7 @@
//! composition duration, Open New Composition), the Composition Navigator bar above the viewer
//! (the flow of nested comps, click to open) and the Composition Mini-Flowchart popup (Tab).
use crate::i18n::{tr, tr_args};
use effectcraft_engine::project::{ItemId, LayerSource, Project};
use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2};
use serde_json::json;
@@ -112,13 +113,13 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let (mut ok, mut cancel) = (false, false);
super::dialogs::modal(ctx, "Pre-compose", vec2(520.0, 360.0), t, |ui| {
ui.horizontal(|ui| {
ui.label("New composition name:");
ui.label(tr("New composition name:"));
let r = ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(260.0));
app.auto.add("dialog.precompose.name", r.rect, "New composition name");
});
ui.add_space(10.0);
ui.add_enabled_ui(d.can_leave, |ui| {
let r = ui.radio_value(&mut d.leave, true, format!("Leave all attributes in \"{}\"", d.layer_name));
let r = ui.radio_value(&mut d.leave, true, tr_args("Leave all attributes in \"{}\"", &[&d.layer_name]));
app.auto.add("dialog.precompose.leave", r.rect, "Leave all attributes");
ui.label(
egui::RichText::new(format!(
@@ -133,24 +134,24 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let r = ui.radio_value(&mut d.leave, false, "Move all attributes into the new composition");
app.auto.add("dialog.precompose.move", r.rect, "Move all attributes");
ui.label(
egui::RichText::new("Use this option to place the currently selected layers together into a new intermediate composition.")
egui::RichText::new(tr("Use this option to place the currently selected layers together into a new intermediate composition."))
.color(t.text_dim)
.size(11.0),
);
ui.add_space(4.0);
ui.add_enabled_ui(!d.leave, |ui| {
let r = ui.checkbox(&mut d.adjust, "Adjust composition duration to the time span of the selected layers");
let r = ui.checkbox(&mut d.adjust, tr("Adjust composition duration to the time span of the selected layers"));
app.auto.add("dialog.precompose.adjust", r.rect, "Adjust composition duration");
});
ui.add_space(8.0);
let r = ui.checkbox(&mut d.open, "Open New Composition");
let r = ui.checkbox(&mut d.open, tr("Open New Composition"));
app.auto.add("dialog.precompose.open", r.rect, "Open New Composition");
ui.add_space(14.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let r = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent));
let r = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
app.auto.add("dialog.precompose.ok", r.rect, "OK");
ok = r.clicked();
let r = ui.button("Cancel");
let r = ui.button(tr("Cancel"));
app.auto.add("dialog.precompose.cancel", r.rect, "Cancel");
cancel = r.clicked();
});
@@ -222,7 +223,7 @@ pub fn navigator(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
let fr = Rect::from_min_size(pos2(rect.max.x - 26.0, rect.min.y + 2.0), vec2(22.0, rect.height() - 4.0));
if crate::widgets::icon_button(ui, fr, crate::icons::Icon::Flowchart, false, &t, egui::Id::new("comp-nav-flow"))
.on_hover_text("Composition Mini-Flowchart")
.on_hover_text(tr("Composition Mini-Flowchart"))
.clicked()
{
app.ui.mini_flowchart = Some([fr.min.x, fr.max.y]);
@@ -300,7 +301,7 @@ pub fn mini_flowchart(app: &mut EffectcraftApp, ctx: &egui::Context) {
}
}
if ups.is_empty() && downs.is_empty() {
p.text(pos2(rect.center().x, rect.max.y - 4.0), Align2::CENTER_BOTTOM, "No nested compositions", Tokens::ui(10.5), t.text_faint);
p.text(pos2(rect.center().x, rect.max.y - 4.0), Align2::CENTER_BOTTOM, tr("No nested compositions"), Tokens::ui(10.5), t.text_faint);
}
});
});
+10 -10
View File
@@ -579,27 +579,27 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
let footage = matches!(it.kind, ItemKind::Footage(_));
resp.context_menu(|ui| {
if ui.button("Delete").clicked() {
if ui.button(crate::i18n::tr("Delete")).clicked() {
actions.push(("project.delete".into(), json!({"items": targets(app, *id, selected)})));
ui.close();
}
if matches!(it.kind, ItemKind::Comp(_)) && ui.button("Open Composition").clicked() {
if matches!(it.kind, ItemKind::Comp(_)) && ui.button(crate::i18n::tr("Open Composition")).clicked() {
actions.push(("comp.open".into(), json!({"comp": id.0})));
ui.close();
}
if ui.button("Rename").clicked() {
if ui.button(crate::i18n::tr("Rename")).clicked() {
ctx.data_mut(|d| d.insert_temp::<Editing>(edit_id(), (id.0, "name".into(), it.name.clone())));
ui.close();
}
if it.parent.is_some() && ui.button("Move to Project Root").clicked() {
if it.parent.is_some() && ui.button(crate::i18n::tr("Move to Project Root")).clicked() {
actions.push(("project.move".into(), json!({"items": [id.0], "folder": null})));
ui.close();
}
if !it.is_folder() && ui.button("Add to Composition").clicked() {
if !it.is_folder() && ui.button(crate::i18n::tr("Add to Composition")).clicked() {
actions.push(("layer.addItem".into(), json!({"item": id.0})));
ui.close();
}
if ui.button("New Comp from Selection").clicked() {
if ui.button(crate::i18n::tr("New Comp from Selection")).clicked() {
if let Some((w, h)) = it.dimensions() {
let d = it.duration().map(|d| d.seconds()).unwrap_or(10.0);
actions.push(("comp.new".into(), json!({"name": format!("{} Comp", it.name), "width": w, "height": h, "duration": d})));
@@ -608,7 +608,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
ui.close();
}
ui.separator();
ui.menu_button("Label", |ui| {
ui.menu_button(crate::i18n::tr("Label"), |ui| {
for l in Label::ALL {
if widgets::label_entry(ui, l, &app.session.prefs.label_name(l), l == it.label, &t).clicked() {
actions.push(("project.setLabel".into(), json!({"items": targets(app, *id, selected), "label": label_index(l)})));
@@ -734,9 +734,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
actions.push(("file.cycleBitDepth".into(), json!({})));
}
}
dresp.on_hover_text("Project color depth: click to cycle 8/16/32 bpc, Alt-click for Project Settings");
dresp.on_hover_text(crate::i18n::tr("Project color depth: click to cycle 8/16/32 bpc, Alt-click for Project Settings"));
let tr = Rect::from_min_size(pos2(foot.max.x - 30.0, foot.min.y + 3.0), vec2(22.0, 22.0));
if widgets::icon_button(ui, tr, Icon::Trash, false, &t, egui::Id::new("ptrash")).on_hover_text("Delete selected project items").clicked()
if widgets::icon_button(ui, tr, Icon::Trash, false, &t, egui::Id::new("ptrash")).on_hover_text(crate::i18n::tr("Delete selected project items")).clicked()
&& !app.session.state.project_selection.is_empty()
{
actions.push(("project.delete".into(), json!({})));
@@ -812,7 +812,7 @@ fn label_index(l: Label) -> usize {
/// The column header's context menu: show or hide the optional columns.
fn column_menu(app: &mut EffectcraftApp, resp: &egui::Response) {
resp.context_menu(|ui| {
ui.label(egui::RichText::new("Columns").weak());
ui.label(egui::RichText::new(crate::i18n::tr("Columns")).weak());
for (key, label, _) in COLUMNS {
let mut on = app.ui.project_columns.iter().any(|c| c == key);
if ui.checkbox(&mut on, label).changed() {
+12 -6
View File
@@ -57,7 +57,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let comp = app.session.active_comp_arc();
let layer = comp.as_ref().and_then(|c| app.session.state.selected_layers.first().and_then(|id| c.layer(*id)).cloned());
let (Some(comp), Some(cid), Some(layer)) = (comp, app.session.active_comp_id(), layer) else {
p.text(rect.center(), Align2::CENTER_CENTER, "Select a layer to see its properties", Tokens::ui(12.0), t.text_faint);
p.text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("Select a layer to see its properties"), Tokens::ui(12.0), t.text_faint);
return;
};
let project = app.session.project.clone();
@@ -76,7 +76,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
section_header(app, ui, &p, x0, w, y, "Layer Transform", "properties.transform");
let rr = Rect::from_min_size(pos2(x0 + w - 40.0, y), vec2(40.0, ROW_H));
let resp = ui.interact(rr, egui::Id::new("props-reset"), Sense::click());
p.text(rr.right_center(), Align2::RIGHT_CENTER, "Reset", Tokens::ui(12.0), if resp.hovered() { t.accent_hover } else { t.accent });
p.text(rr.right_center(), Align2::RIGHT_CENTER, crate::i18n::tr("Reset"), Tokens::ui(12.0), if resp.hovered() { t.accent_hover } else { t.accent });
app.auto.add("properties.transform.reset", rr, "Reset Transform");
if resp.clicked() {
for pr in tr.props() {
@@ -112,7 +112,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let resp = ui.interact(br, egui::Id::new("props-add-animator"), Sense::click());
p.rect_stroke(br, 4.0, Stroke::new(1.0, if resp.hovered() { t.text_dim } else { t.field_border }), egui::StrokeKind::Inside);
icons::paint(&p, Rect::from_center_size(pos2(br.min.x + 14.0, br.center().y), vec2(10.0, 10.0)), Icon::Plus, t.text);
p.text(pos2(br.min.x + 26.0, br.center().y), Align2::LEFT_CENTER, "Add Animator", Tokens::ui(12.0), t.text);
p.text(pos2(br.min.x + 26.0, br.center().y), Align2::LEFT_CENTER, crate::i18n::tr("Add Animator"), Tokens::ui(12.0), t.text);
app.auto.add("properties.addAnimator", br, "Add Animator");
let pop = egui::Id::new("props-add-animator-pop");
if resp.clicked() {
@@ -132,7 +132,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let pr = Rect::from_min_size(pos2(br.max.x + 8.0, y), vec2(84.0, 24.0));
let presp = ui.interact(pr, egui::Id::new("props-text-presets"), Sense::click());
p.rect_stroke(pr, 4.0, Stroke::new(1.0, if presp.hovered() { t.text_dim } else { t.field_border }), egui::StrokeKind::Inside);
p.text(pr.center(), Align2::CENTER_CENTER, "Presets", Tokens::ui(12.0), t.text);
p.text(pr.center(), Align2::CENTER_CENTER, crate::i18n::tr("Presets"), Tokens::ui(12.0), t.text);
app.auto.add("properties.textPresets", pr, "Text Animation Presets");
let ppop = egui::Id::new("props-text-presets-pop");
if presp.clicked() {
@@ -362,7 +362,13 @@ fn text_section(
p.text(pos2(col2, y + 9.0), Align2::LEFT_CENTER, "A", Tokens::semibold(11.0), t.text_dim);
let r = Rect::from_min_size(pos2(col2 + 26.0, y), vec2(40.0, 18.0));
let resp = ui.interact(r, egui::Id::new("props-leading-auto"), Sense::click());
p.text(pos2(r.min.x + 2.0, r.center().y), Align2::LEFT_CENTER, "Auto", Tokens::ui(12.0), if resp.hovered() { t.accent_hover } else { t.hot_text });
p.text(
pos2(r.min.x + 2.0, r.center().y),
Align2::LEFT_CENTER,
crate::i18n::tr("Auto"),
Tokens::ui(12.0),
if resp.hovered() { t.accent_hover } else { t.hot_text },
);
app.auto.add("properties.text.leading", r, "leading");
if resp.clicked() {
set(actions, json!({"leading": (doc.size * 1.2).round()}));
@@ -480,7 +486,7 @@ fn more_button(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, x
let resp = ui.interact(r, egui::Id::new(id), Sense::click());
p.rect_filled(r, 4.0, if resp.hovered() { t.hover } else { t.field_bg });
p.text(pos2(r.min.x + 8.0, r.center().y), Align2::LEFT_CENTER, "···", Tokens::semibold(12.0), t.text);
p.text(pos2(r.min.x + 26.0, r.center().y), Align2::LEFT_CENTER, "More", Tokens::ui(12.0), t.text);
p.text(pos2(r.min.x + 26.0, r.center().y), Align2::LEFT_CENTER, crate::i18n::tr("More"), Tokens::ui(12.0), t.text);
app.auto.add(auto, r, "More");
if resp.clicked() {
app.show_panel(panel);
+27 -26
View File
@@ -11,6 +11,7 @@
//! every setting; entries needing numbers (custom time span and frame rate, crop, resize, bitrate,
//! key frame interval, Make Template) open a form that runs the same command.
use crate::i18n::{tr, tr_args};
use effectcraft_engine::project::render_queue::{
AlphaMode, AudioFormat, AudioOutput, Channels, ColorDepth, CurrentOrOff, EffectsMode, FieldRender, OpusApplication, OutputFormat, PostRenderAction,
ProResProfile, Pulldown, RESIZE_PRESETS, RenderLog, RenderQuality, RenderQueueItem, RenderStatus, ResizeQuality, SwitchOverride, TimeSpan, WebmVideoCodec,
@@ -78,33 +79,33 @@ fn settings_menu(it: &RenderQueueItem, templates: &RenderTemplates) -> Vec<(Stri
}
v.push(("-".into(), Value::Null));
for (u, k) in effectcraft_engine::project::render_queue::ProxyUse::ALL.iter().zip(["current", "all", "comp", "none"]) {
v.push((mark(s.proxy_use == *u, &format!("Proxy Use: {}", u.label())), json!({"proxyUse": k})));
v.push((mark(s.proxy_use == *u, &tr_args("Proxy Use: {}", &[&(u.label())])), json!({"proxyUse": k})));
}
for (e, k) in EffectsMode::ALL.iter().zip(["current", "allOn", "allOff"]) {
v.push((mark(s.effects == *e, &format!("Effects: {}", e.label())), json!({"effects": k})));
v.push((mark(s.effects == *e, &tr_args("Effects: {}", &[&(e.label())])), json!({"effects": k})));
}
for (e, k) in [(CurrentOrOff::Current, "current"), (CurrentOrOff::AllOff, "allOff")] {
v.push((mark(s.solo == e, &format!("Solo Switches: {}", e.label())), json!({"solo": k})));
v.push((mark(s.solo == e, &tr_args("Solo Switches: {}", &[&(e.label())])), json!({"solo": k})));
}
for (e, k) in [(CurrentOrOff::Current, "current"), (CurrentOrOff::AllOff, "allOff")] {
v.push((mark(s.guide_layers == e, &format!("Guide Layers: {}", e.label())), json!({"guideLayers": k})));
v.push((mark(s.guide_layers == e, &tr_args("Guide Layers: {}", &[&(e.label())])), json!({"guideLayers": k})));
}
for (d, k) in ColorDepth::ALL.iter().zip(["current", "8", "16", "32"]) {
v.push((mark(s.color_depth == *d, &format!("Color Depth: {}", d.label())), json!({"colorDepth": k})));
v.push((mark(s.color_depth == *d, &tr_args("Color Depth: {}", &[&(d.label())])), json!({"colorDepth": k})));
}
v.push(("-".into(), Value::Null));
for (m, k) in SwitchOverride::ALL.iter().zip(["current", "onForChecked", "offForAll"]) {
v.push((mark(s.frame_blending == *m, &format!("Frame Blending: {}", m.label())), json!({"frameBlending": k})));
v.push((mark(s.frame_blending == *m, &tr_args("Frame Blending: {}", &[&(m.label())])), json!({"frameBlending": k})));
}
for (m, k) in SwitchOverride::ALL.iter().zip(["current", "onForChecked", "offForAll"]) {
v.push((mark(s.motion_blur_override() == *m, &format!("Motion Blur: {}", m.label())), json!({"motionBlur": k})));
v.push((mark(s.motion_blur_override() == *m, &tr_args("Motion Blur: {}", &[&(m.label())])), json!({"motionBlur": k})));
}
for (f, k) in FieldRender::ALL.iter().zip(["off", "upper", "lower"]) {
v.push((mark(s.field_render == *f, &format!("Field Render: {}", f.label())), json!({"fieldRender": k})));
v.push((mark(s.field_render == *f, &tr_args("Field Render: {}", &[&(f.label())])), json!({"fieldRender": k})));
}
if s.field_render != FieldRender::Off {
for pd in Pulldown::ALL {
v.push((mark(s.pulldown == pd, &format!("3:2 Pulldown: {}", pd.label())), json!({"pulldown": pd.label()})));
v.push((mark(s.pulldown == pd, &tr_args("3:2 Pulldown: {}", &[&(pd.label())])), json!({"pulldown": pd.label()})));
}
}
v.push(("-".into(), Value::Null));
@@ -136,7 +137,7 @@ fn output_menu(it: &RenderQueueItem, available: &[OutputFormat], templates: &Ren
v.push(("-".into(), Value::Null));
for f in OutputFormat::ALL {
if available.contains(&f) {
v.push((mark(o.format == f, &format!("Format: {}", f.label())), json!({"format": format!("{f:?}")})));
v.push((mark(o.format == f, &tr_args("Format: {}", &[&(f.label())])), json!({"format": format!("{f:?}")})));
}
}
v.push(("-".into(), Value::Null));
@@ -168,7 +169,7 @@ fn output_menu(it: &RenderQueueItem, available: &[OutputFormat], templates: &Ren
OutputFormat::WebM => {
v.push(("-".into(), Value::Null));
for c in [WebmVideoCodec::Vp9, WebmVideoCodec::Av1] {
v.push((mark(o.webm_codec == c, &format!("Video Codec: {}", c.label())), json!({"webmCodec": c.label().to_ascii_lowercase()})));
v.push((mark(o.webm_codec == c, &tr_args("Video Codec: {}", &[&(c.label())])), json!({"webmCodec": c.label().to_ascii_lowercase()})));
}
if o.webm_codec == WebmVideoCodec::Av1 {
codec_menu(o, &mut v);
@@ -185,7 +186,7 @@ fn output_menu(it: &RenderQueueItem, available: &[OutputFormat], templates: &Ren
v.push((mark(o.opus_bitrate_kbps == kbps, &format!("Opus Bitrate: {kbps} kbps")), json!({"audioBitrate": kbps})));
}
for (a, k) in [(OpusApplication::Audio, "audio"), (OpusApplication::Voip, "voice")] {
v.push((mark(o.opus_application == a, &format!("Opus Tuning: {}", a.label())), json!({"opusApplication": k})));
v.push((mark(o.opus_application == a, &tr_args("Opus Tuning: {}", &[&(a.label())])), json!({"opusApplication": k})));
}
}
OutputFormat::JpegSequence => {
@@ -226,7 +227,7 @@ fn output_menu(it: &RenderQueueItem, available: &[OutputFormat], templates: &Ren
(PostRenderAction::ImportAndReplace, "importAndReplace"),
(PostRenderAction::SetProxy, "setProxy"),
] {
v.push((mark(it.post_render == a, &format!("Post-Render Action: {}", a.label())), json!({"postRenderAction": k})));
v.push((mark(it.post_render == a, &tr_args("Post-Render Action: {}", &[&(a.label())])), json!({"postRenderAction": k})));
}
v.push((mark(o.include_project_link, "Include Project Link"), json!({"includeProjectLink": !o.include_project_link})));
if o.format.supports_audio() {
@@ -246,7 +247,7 @@ fn output_menu(it: &RenderQueueItem, available: &[OutputFormat], templates: &Ren
v.push((mark(o.audio_channels != 1, "Audio Channels: Stereo"), json!({"audioChannels": "stereo"})));
if matches!(o.format, OutputFormat::ProRes | OutputFormat::Wav | OutputFormat::Aiff) {
for f in [AudioFormat::S16, AudioFormat::S24, AudioFormat::F32] {
v.push((mark(o.audio_format == f, &format!("Sample Format: {}", f.label())), json!({"audioFormat": f.label()})));
v.push((mark(o.audio_format == f, &tr_args("Sample Format: {}", &[&(f.label())])), json!({"audioFormat": f.label()})));
}
}
}
@@ -259,7 +260,7 @@ fn codec_menu(o: &effectcraft_engine::project::render_queue::OutputModule, v: &m
let c = &o.codec;
v.push(("-".into(), Value::Null));
for p in CodecProfile::ALL {
v.push((mark(c.profile == p, &format!("Profile: {}", p.label())), json!({"profile": format!("{p:?}").to_ascii_lowercase()})));
v.push((mark(c.profile == p, &tr_args("Profile: {}", &[&(p.label())])), json!({"profile": format!("{p:?}").to_ascii_lowercase()})));
}
v.push(("-".into(), Value::Null));
v.push((mark(c.level.is_none(), "Level: Auto"), json!({"level": "auto"})));
@@ -271,7 +272,7 @@ fn codec_menu(o: &effectcraft_engine::project::render_queue::OutputModule, v: &m
}
v.push(("-".into(), Value::Null));
for r in [RateControlMode::Bitrate, RateControlMode::Quality] {
v.push((mark(c.rate_control == r, &format!("Rate Control: {}", r.label())), json!({"rateControl": format!("{r:?}").to_ascii_lowercase()})));
v.push((mark(c.rate_control == r, &tr_args("Rate Control: {}", &[&(r.label())])), json!({"rateControl": format!("{r:?}").to_ascii_lowercase()})));
}
if c.rate_control == RateControlMode::Bitrate {
for kbps in [1_000u32, 2_000, 5_000, 10_000, 20_000, 40_000] {
@@ -438,7 +439,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let top = Rect::from_min_size(rect.min, vec2(rect.width(), TOP_H));
p.rect_filled(top, 0.0, t.tl_ruler_bg);
let x0 = rect.min.x + 12.0;
p.text(pos2(x0, top.min.y + 16.0), Align2::LEFT_CENTER, "Current Render", Tokens::semibold(12.5), t.text);
p.text(pos2(x0, top.min.y + 16.0), Align2::LEFT_CENTER, tr("Current Render"), Tokens::semibold(12.5), t.text);
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 {
@@ -465,7 +466,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if widgets::checkbox(ui, nb, notify, &t, egui::Id::new("rq-notify")).clicked() {
actions.push(("renderQueue.setNotify", json!({"notify": !notify})));
}
p.text(pos2(nb.max.x + 5.0, nb.center().y), Align2::LEFT_CENTER, "Notify", Tokens::ui(11.5), t.text_dim);
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() {
@@ -531,7 +532,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
ui.set_clip_rect(list.intersect(panel_clip));
let mut y = list.min.y - scroll.offset;
if queue.is_empty() {
lp.text(pos2(list.center().x, list.min.y + 30.0), Align2::CENTER_CENTER, "The render queue is empty.", Tokens::ui(12.0), t.text_faint);
lp.text(pos2(list.center().x, list.min.y + 30.0), Align2::CENTER_CENTER, tr("The render queue is empty."), Tokens::ui(12.0), t.text_faint);
}
let editing_id = egui::Id::new("rq-edit-output");
let mut editing: Option<(u64, String)> = ui.data(|d| d.get_temp(editing_id));
@@ -559,19 +560,19 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
let item_id = it.id;
row_resp.context_menu(|ui| {
if ui.button("Duplicate").clicked() {
if ui.button(tr("Duplicate")).clicked() {
actions.push(("renderQueue.duplicate", json!({"item": item_id})));
ui.close();
}
if ui.button("Remove").clicked() {
if ui.button(tr("Remove")).clicked() {
actions.push(("renderQueue.remove", json!({"item": item_id})));
ui.close();
}
if k > 0 && ui.button("Move Up").clicked() {
if k > 0 && ui.button(tr("Move Up")).clicked() {
actions.push(("renderQueue.move", json!({"item": item_id, "to": n - 1})));
ui.close();
}
if n < queue.len() && ui.button("Move Down").clicked() {
if n < queue.len() && ui.button(tr("Move Down")).clicked() {
actions.push(("renderQueue.move", json!({"item": item_id, "to": n + 1})));
ui.close();
}
@@ -670,7 +671,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
// Log: Errors Only / Plus Settings / Plus Per Frame Info.
let ld = Rect::from_min_size(pos2(r.max.x - 190.0, r.min.y + 3.0), vec2(150.0, ROW_H - 6.0));
lp.text(pos2(ld.min.x - 6.0, r.center().y), Align2::RIGHT_CENTER, "Log:", Tokens::ui(11.5), t.text_dim);
lp.text(pos2(ld.min.x - 6.0, r.center().y), Align2::RIGHT_CENTER, tr("Log:"), Tokens::ui(11.5), t.text_dim);
let lid = egui::Id::new(("rq-log", it.id));
if widgets::dropdown(ui, ld, it.log.label(), &t, lid).clicked() && !rendering {
widgets::open_popup(ui, lid);
@@ -684,7 +685,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
// Output To.
let ox = dd.max.x + 14.0;
lp.text(pos2(ox, r.center().y), Align2::LEFT_CENTER, "Output To:", Tokens::ui(11.5), t.text_dim);
lp.text(pos2(ox, r.center().y), Align2::LEFT_CENTER, tr("Output To:"), Tokens::ui(11.5), t.text_dim);
let odd = Rect::from_min_size(pos2(ox + 62.0, r.min.y + 3.0), vec2(22.0, ROW_H - 6.0));
let oid = egui::Id::new(("rq-out", it.id));
if widgets::dropdown(ui, odd, "", &t, oid).clicked() && !rendering {
@@ -737,7 +738,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
}
let how = if dialog { "Click to choose where to save; Alt-click to edit the template" } else { "Click to edit" };
resp.on_hover_text(format!("Template: {}\n{how}", it.output.output));
resp.on_hover_text(tr_args("Template: {}\n{}", &[&it.output.output, &how]));
app.auto.add(&aid("outputPath"), path_rect, &shown);
}
}
+11 -10
View File
@@ -6,6 +6,7 @@
//! `templates.default.<Slot>`, `templates.select`, `templates.duplicate`, `templates.delete`,
//! `templates.newName`, `templates.saveFromItem`, `templates.ok`.
use crate::i18n::{tr, tr_args};
use effectcraft_engine::project::render_templates::{RenderTemplates, TemplateKind, TemplateSlot};
use egui::{Color32, vec2};
use serde_json::{Value, json};
@@ -51,7 +52,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let mut close = false;
let title = if st.output { "Output Module Templates" } else { "Render Settings Templates" };
super::dialogs::modal(ctx, title, vec2(600.0, 560.0), t, |ui| {
ui.label(egui::RichText::new("Defaults").font(Tokens::semibold(12.5)).color(t.text));
ui.label(egui::RichText::new(tr("Defaults")).font(Tokens::semibold(12.5)).color(t.text));
egui::Grid::new("tpl-defaults").num_columns(2).spacing(vec2(14.0, 6.0)).show(ui, |ui| {
for slot in TemplateSlot::ALL {
ui.label(slot.label());
@@ -71,9 +72,9 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
});
ui.add_space(12.0);
ui.separator();
ui.label(egui::RichText::new("Settings").font(Tokens::semibold(12.5)).color(t.text));
ui.label(egui::RichText::new(tr("Settings")).font(Tokens::semibold(12.5)).color(t.text));
ui.horizontal(|ui| {
ui.label("Settings Name:");
ui.label(tr("Settings Name:"));
let r = egui::ComboBox::from_id_salt("tpl-select").selected_text(&st.selected).width(300.0).show_ui(ui, |ui| {
for n in &list {
ui.selectable_value(&mut st.selected, n.clone(), n);
@@ -106,7 +107,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
});
});
ui.horizontal(|ui| {
let r = ui.button("Duplicate");
let r = ui.button(tr("Duplicate"));
app.auto.add("templates.duplicate", r.rect, "Duplicate");
if r.clicked() {
let mut n = format!("{} copy", st.selected);
@@ -118,13 +119,13 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
actions.push(("renderQueue.saveTemplate", json!({"kind": kind, "name": n, "from": st.selected})));
st.selected = n;
}
let r = ui.add_enabled(!builtin, egui::Button::new("Delete"));
let r = ui.add_enabled(!builtin, egui::Button::new(tr("Delete")));
app.auto.add("templates.delete", r.rect, "Delete");
if r.clicked() {
actions.push(("renderQueue.deleteTemplate", json!({"kind": kind, "name": st.selected})));
}
if builtin {
ui.label(egui::RichText::new("built-in").color(t.text_faint).size(11.0));
ui.label(egui::RichText::new(tr("built-in")).color(t.text_faint).size(11.0));
}
});
ui.add_space(8.0);
@@ -132,12 +133,12 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
// New template from the selected (or only) Render Queue item.
let queue = &app.session.project.render_queue;
ui.horizontal(|ui| {
ui.label("New from Render Queue item:");
let r = ui.add(egui::TextEdit::singleline(&mut st.new_name).hint_text("template name").desired_width(180.0));
ui.label(tr("New from Render Queue item:"));
let r = ui.add(egui::TextEdit::singleline(&mut st.new_name).hint_text(tr("template name")).desired_width(180.0));
app.auto.add("templates.newName", r.rect, &st.new_name);
let item = queue.last().map(|i| i.id);
let ok = item.is_some() && !st.new_name.trim().is_empty();
let r = ui.add_enabled(ok, egui::Button::new(format!("Save from #{}", queue.len().max(1))));
let r = ui.add_enabled(ok, egui::Button::new(tr_args("Save from #{}", &[&(queue.len().max(1))])));
app.auto.add("templates.saveFromItem", r.rect, "Save from item");
if r.clicked()
&& let Some(id) = item
@@ -149,7 +150,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
});
ui.add_space(12.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let r = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent));
let r = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
app.auto.add("templates.ok", r.rect, "OK");
close = r.clicked();
});
+2 -1
View File
@@ -3,6 +3,7 @@
//! time, with the colour standard (Rec. 601 / 709 / 2020), 8-bit or float scale and clamp. The
//! maths is `effectcraft_raster::scopes`; agents read the same numbers with `scopes.analyze`.
use crate::i18n::tr;
use effectcraft_engine::commands::panels_cmds::scope_frame;
use effectcraft_engine::raster::scopes::{self, ColorStandard, Scope, ScopeKind, ScopeOpts};
use egui::{Align2, Color32, Rect, Stroke, pos2, vec2};
@@ -75,7 +76,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
p.rect_filled(area, 2.0, Color32::from_rgb(0x10, 0x10, 0x10));
app.auto.add("scopes.plot", area, kind.label());
let Some(cid) = app.session.active_comp_id() else {
p.text(area.center(), Align2::CENTER_CENTER, "Open a composition to see its scopes", Tokens::ui(12.0), t.text_faint);
p.text(area.center(), Align2::CENTER_CENTER, tr("Open a composition to see its scopes"), Tokens::ui(12.0), t.text_faint);
return;
};
// Plot rectangle: square for vectorscopes.
+4 -3
View File
@@ -2,6 +2,7 @@
//! (`app.project`, comps, layers, properties…). Each run goes through the `script.run` command
//! in the console's persistent context, so variables survive between runs; edits are undoable.
use crate::i18n::tr;
use egui::{Align2, Rect, Sense, pos2, vec2};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
@@ -113,7 +114,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
.font(Tokens::mono(12.0))
.desired_width(in_rect.width())
.desired_rows(4)
.hint_text("Script… (⌘/Ctrl+Enter runs)");
.hint_text(tr("Script… (⌘/Ctrl+Enter runs)"));
let resp = child.add_sized(in_rect.size(), te);
if resp.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter) && i.modifiers.command) {
run_now = true;
@@ -125,10 +126,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let clear_r = Rect::from_min_size(pos2(bar.max.x - 128.0, bar.min.y + 2.0), vec2(56.0, 20.0));
let run_resp = ui.interact(run_r, egui::Id::new("script-console-run"), Sense::click());
p.rect_filled(run_r, 3.0, if run_resp.hovered() { t.accent_hover } else { t.accent });
p.text(run_r.center(), Align2::CENTER_CENTER, "Run", Tokens::ui(12.0), egui::Color32::WHITE);
p.text(run_r.center(), Align2::CENTER_CENTER, tr("Run"), Tokens::ui(12.0), egui::Color32::WHITE);
let clear_resp = ui.interact(clear_r, egui::Id::new("script-console-clear"), Sense::click());
p.rect_stroke(clear_r, 3.0, egui::Stroke::new(1.0, t.field_border), egui::StrokeKind::Inside);
p.text(clear_r.center(), Align2::CENTER_CENTER, "Clear", Tokens::ui(12.0), t.text);
p.text(clear_r.center(), Align2::CENTER_CENTER, tr("Clear"), Tokens::ui(12.0), t.text);
app.auto.add("scriptConsole.run", run_r, "Run");
app.auto.add("scriptConsole.clear", clear_r, "Clear");
if run_resp.clicked() {
+2 -1
View File
@@ -6,6 +6,7 @@
//! named it). Controls with an `onDraw` handler are painted from their draw list; edit texts and
//! sliders send live `changing` updates (onChanging) while typing / dragging.
use crate::i18n::tr;
use effectcraft_engine::Services;
use effectcraft_engine::scriptui::{DrawOp, ImageRef, PathSeg, ScriptWindow, Widget, WidgetKind, WindowKind, layout};
use egui::{Align2, Color32, Rect, Sense, Stroke, StrokeKind, UiBuilder, pos2, vec2};
@@ -410,7 +411,7 @@ pub fn show_windows(app: &mut EffectcraftApp, ctx: &egui::Context) {
pub fn panel(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect, id: u32) {
let t = app.tokens;
let Some(mut w) = app.session.script_ui.window(id).cloned() else {
ui.painter().text(rect.center(), Align2::CENTER_CENTER, "This ScriptUI panel has closed", Tokens::ui(12.0), t.text_faint);
ui.painter().text(rect.center(), Align2::CENTER_CENTER, tr("This ScriptUI panel has closed"), Tokens::ui(12.0), t.text_faint);
return;
};
// Docked panels fill their frame (ScriptUI lays them out at the panel's size).
+31 -29
View File
@@ -7,6 +7,7 @@
//! `settings.labels.<n>.name` / `.color`, `settings.button.<label>`, `settings.ok`,
//! `settings.cancel`, `settings.previous`, `settings.next`.
use crate::i18n::{tr, tr_args};
use effectcraft_engine::prefs::{Item, Kind, Page, pages};
use effectcraft_engine::segment::Task;
use egui::{Color32, RichText, vec2};
@@ -69,10 +70,10 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
let devices: Vec<String> = app.hooks.audio_devices.as_ref().map(|f| f()).unwrap_or_default();
let sel = get(key).as_str().unwrap_or("").to_string();
ui.horizontal(|ui| {
ui.add_sized(vec2(label_w, 18.0), egui::Label::new("Default Output:"));
ui.add_sized(vec2(label_w, 18.0), egui::Label::new(tr("Default Output:")));
let shown = if sel.is_empty() { "System Default".to_string() } else { sel.clone() };
let r = egui::ComboBox::from_id_salt(("settings", key)).selected_text(shown).width(260.0).show_ui(ui, |ui| {
if ui.selectable_label(sel.is_empty(), "System Default").clicked() {
if ui.selectable_label(sel.is_empty(), tr("System Default")).clicked() {
acts.push(Act::Set(key.into(), json!("")));
}
for d in &devices {
@@ -84,7 +85,7 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
reg(app, &format!("settings.{key}"), &r.response, "Default Output");
});
if devices.is_empty() {
ui.label(RichText::new("No device list from this host; the system default output is used.").color(t.text_dim));
ui.label(RichText::new(tr("No device list from this host; the system default output is used.")).color(t.text_dim));
}
}
Item::Setting { key, label, kind, live } => {
@@ -129,9 +130,9 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
}
Kind::Slider(lo, hi) => {
let mut x = v.as_f64().unwrap_or(0.0);
ui.label(RichText::new("Darker").color(t.text_dim));
ui.label(RichText::new(tr("Darker")).color(t.text_dim));
let r = ui.add(egui::Slider::new(&mut x, lo..=hi).show_value(false));
ui.label(RichText::new("Lighter").color(t.text_dim));
ui.label(RichText::new(tr("Lighter")).color(t.text_dim));
if ui.small_button("Default").clicked() {
acts.push(Act::Set(key.into(), json!(0.0)));
}
@@ -163,7 +164,7 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value
acts.push(Act::Set(key.into(), json!(s)));
}
if kind == Kind::Path {
let b = ui.button("Choose...");
let b = ui.button(tr("Choose..."));
reg(app, &format!("{id}.choose"), &b, "Choose...");
if b.clicked() {
acts.push(if key.ends_with("Path") { Act::PickProject(key.into()) } else { Act::PickFolder(key.into()) });
@@ -253,7 +254,7 @@ fn models_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, t: &Tokens, acts: &mut
ui.label(RichText::new(format!("By {by}")).color(t.text_dim));
}
ui.horizontal_wrapped(|ui| {
ui.label(RichText::new(format!("Licence: {}", m["licence"].as_str().unwrap_or_default())).color(t.text_dim));
ui.label(RichText::new(tr_args("Licence: {}", &[&(m["licence"].as_str().unwrap_or_default())])).color(t.text_dim));
if let Some(size) = m["size"].as_u64() {
ui.label(RichText::new(format!("· {}", human_bytes(size))).color(t.text_dim));
}
@@ -271,21 +272,21 @@ fn models_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, t: &Tokens, acts: &mut
}
ui.horizontal(|ui| {
if !installed && !web {
let r = ui.add_enabled(busy.is_none(), egui::Button::new("Download"));
let r = ui.add_enabled(busy.is_none(), egui::Button::new(tr("Download")));
reg(app, &format!("settings.{prefix}.download.{id}"), &r, "Download");
if r.clicked() {
acts.push(Act::Run(format!("{prefix}.model.download"), json!({"id": id})));
}
}
if !web {
let r = ui.add_enabled(busy.is_none(), egui::Button::new("Install from File…"));
let r = ui.add_enabled(busy.is_none(), egui::Button::new(tr("Install from File…")));
reg(app, &format!("settings.{prefix}.install.{id}"), &r, "Install from File");
if r.clicked() {
acts.push(Act::PickModel(task));
}
}
if installed {
let r = ui.add_enabled(busy.is_none(), egui::Button::new("Remove"));
let r = ui.add_enabled(busy.is_none(), egui::Button::new(tr("Remove")));
reg(app, &format!("settings.{prefix}.remove.{id}"), &r, "Remove");
if r.clicked() {
acts.push(Act::Run(format!("{prefix}.model.remove"), json!({"id": id})));
@@ -305,9 +306,9 @@ fn models_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, t: &Tokens, acts: &mut
ui.label(RichText::new(e).color(t.danger));
}
if web {
ui.label(RichText::new("Trained models are available in the desktop app; the browser uses the classic engine.").color(t.text_dim));
ui.label(RichText::new(tr("Trained models are available in the desktop app; the browser uses the classic engine.")).color(t.text_dim));
} else if let Some(f) = info["folder"].as_str() {
ui.label(RichText::new(format!("Models folder: {f}")).color(t.text_faint));
ui.label(RichText::new(tr_args("Models folder: {}", &[&f])).color(t.text_faint));
}
}
@@ -318,7 +319,7 @@ fn browser_storage_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, t: &Tokens, a
let info = host.info();
let u = |v: &Value| v.as_u64().unwrap_or(0);
ui.add_space(6.0);
ui.label(RichText::new("Browser Storage").font(Tokens::semibold(12.5)).color(t.tab_text_active));
ui.label(RichText::new(tr("Browser Storage")).font(Tokens::semibold(12.5)).color(t.tab_text_active));
ui.separator();
let backend = match info["backend"].as_str().unwrap_or("") {
"opfs" => "Origin Private File System",
@@ -327,7 +328,7 @@ fn browser_storage_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, t: &Tokens, a
"" => "Unknown",
other => other,
};
let r = ui.label(format!("Stored in: {backend}"));
let r = ui.label(tr_args("Stored in: {}", &[&backend]));
reg(app, "settings.storage.backend", &r, backend);
let (usage, quota) = (u(&info["usage"]), u(&info["quota"]));
let frac = if quota > 0 { usage as f32 / quota as f32 } else { 0.0 };
@@ -342,7 +343,8 @@ fn browser_storage_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, t: &Tokens, a
let f = &info["files"];
let row = |what: &str, k: &str| format!("{what}: {} ({})", u(&f[k]["count"]), human_bytes(u(&f[k]["bytes"])));
ui.label(
RichText::new(format!("{} · {} · {}", row("Projects", "projects"), row("Imported media", "media"), row("Auto-saves", "autoSaves"))).color(t.text_dim),
RichText::new(format!("{} · {} · {}", row(tr("Projects"), "projects"), row(tr("Imported media"), "media"), row(tr("Auto-saves"), "autoSaves")))
.color(t.text_dim),
);
let d = &info["diskCache"];
let dc = if d["enabled"].as_bool().unwrap_or(false) {
@@ -364,7 +366,7 @@ fn browser_storage_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, t: &Tokens, a
let state = if persisted { "Persistent storage: granted" } else { "Persistent storage: not granted (the browser may clear it when space runs low)" };
ui.label(state);
if !persisted {
let r = ui.button("Request Persistent Storage");
let r = ui.button(tr("Request Persistent Storage"));
reg(app, "settings.storage.persist", &r, "Request Persistent Storage");
if r.clicked() {
acts.push(Act::Run("storage.persist".into(), json!({})));
@@ -387,7 +389,7 @@ fn browser_storage_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, t: &Tokens, a
}
fn labels_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, cur: &Value, acts: &mut Vec<Act>) {
ui.label("Label Colors and Names:");
ui.label(tr("Label Colors and Names:"));
ui.add_space(4.0);
let labels = cur.get("labels").and_then(Value::as_array).cloned().unwrap_or_default();
egui::Grid::new("settings-labels").num_columns(4).spacing([10.0, 4.0]).show(ui, |ui| {
@@ -411,7 +413,7 @@ fn labels_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, cur: &Value, acts: &mu
}
});
ui.add_space(8.0);
let r = ui.button("Reset Labels");
let r = ui.button(tr("Reset Labels"));
app.auto.add("settings.button.Reset Labels", r.rect, "Reset Labels");
if r.clicked() {
acts.push(Act::Run("prefs.reset".into(), json!({"page": "labels"})));
@@ -474,21 +476,21 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
ui.add_space(8.0);
ui.separator();
ui.horizontal(|ui| {
let r = ui.add_enabled(idx > 0, egui::Button::new("Previous"));
let r = ui.add_enabled(idx > 0, egui::Button::new(tr("Previous")));
app.auto.add("settings.previous", r.rect, "Previous");
if r.clicked() {
page = all[idx - 1].id.to_string();
}
let r = ui.add_enabled(idx + 1 < all.len(), egui::Button::new("Next"));
let r = ui.add_enabled(idx + 1 < all.len(), egui::Button::new(tr("Next")));
app.auto.add("settings.next", r.rect, "Next");
if r.clicked() {
page = all[idx + 1].id.to_string();
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let r = ui.add(egui::Button::new(RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent));
let r = ui.add(egui::Button::new(RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
app.auto.add("settings.ok", r.rect, "OK");
ok |= r.clicked();
let r = ui.button("Cancel");
let r = ui.button(tr("Cancel"));
app.auto.add("settings.cancel", r.rect, "Cancel");
cancel |= r.clicked();
});
@@ -562,10 +564,10 @@ pub fn recovery(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
};
let mut choice: Option<&str> = None;
super::dialogs::modal(ctx, "Recover Project", vec2(560.0, 230.0), t, |ui| {
ui.label("EffectCraft didn't quit normally last time.");
ui.label(tr("EffectCraft didn't quit normally last time."));
ui.add_space(6.0);
if let Some(p) = &r.project {
ui.label(format!("Project: {p}"));
ui.label(tr_args("Project: {}", &[&p]));
}
if let Some(a) = &r.autosave {
let when = std::fs::metadata(a).and_then(|m| m.modified()).ok().and_then(|m| m.elapsed().ok()).map(|d| {
@@ -578,26 +580,26 @@ pub fn recovery(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
format!("{} hours ago", m / 60)
}
});
ui.label(format!("Latest auto-save: {a}"));
ui.label(tr_args("Latest auto-save: {}", &[&a]));
if let Some(w) = when {
ui.label(RichText::new(format!("Saved {w}")).color(t.text_dim));
ui.label(RichText::new(tr_args("Saved {}", &[&w])).color(t.text_dim));
}
}
ui.add_space(14.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let b = ui.add(egui::Button::new(RichText::new(" Open Auto-Save ").color(Color32::WHITE)).fill(t.accent));
let b = ui.add(egui::Button::new(RichText::new(tr(" Open Auto-Save ")).color(Color32::WHITE)).fill(t.accent));
app.auto.add("recovery.openAutoSave", b.rect, "Open Auto-Save");
if b.clicked() {
choice = Some("autosave");
}
if r.project.is_some() {
let b = ui.button("Open Last Saved Project");
let b = ui.button(tr("Open Last Saved Project"));
app.auto.add("recovery.openProject", b.rect, "Open Last Saved Project");
if b.clicked() {
choice = Some("project");
}
}
let b = ui.button("Don't Recover");
let b = ui.button(tr("Don't Recover"));
app.auto.add("recovery.dismiss", b.rect, "Don't Recover");
if b.clicked() {
choice = Some("none");
+22 -21
View File
@@ -15,6 +15,7 @@
//! `shortcuts.rename`, `shortcuts.delete`, `shortcuts.import`, `shortcuts.export`,
//! `shortcuts.resetPreset`, `shortcuts.close`.
use crate::i18n::{tr, tr_args};
use effectcraft_engine::shortcuts::{APP_SCOPE, Bindable, DEFAULT_PRESET, normalize};
use egui::{Align2, Color32, Rect, RichText, Sense, Stroke, StrokeKind, pos2, vec2};
use serde_json::{Value, json};
@@ -237,7 +238,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
super::dialogs::modal(ctx, "Keyboard Shortcuts", vec2(1000.0, 720.0), t, |ui| {
// ---- preset bar
ui.horizontal(|ui| {
ui.label("Preset:");
ui.label(tr("Preset:"));
let r = egui::ComboBox::from_id_salt("sc-preset").selected_text(&active).width(220.0).show_ui(ui, |ui| {
for n in &presets {
if ui.selectable_label(*n == active, n).clicked() {
@@ -247,7 +248,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
});
app.auto.add("shortcuts.preset", r.response.rect, "Preset");
let custom = active != DEFAULT_PRESET;
let b = ui.button("Duplicate");
let b = ui.button(tr("Duplicate"));
app.auto.add("shortcuts.duplicate", b.rect, "Duplicate");
if b.clicked() {
run.push(("shortcuts.preset".into(), json!({"op": "duplicate", "from": active, "name": format!("{active} copy")})));
@@ -255,35 +256,35 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
if let Some(name) = &mut st.rename {
let r = ui.add(egui::TextEdit::singleline(name).desired_width(140.0));
app.auto.add("shortcuts.renameField", r.rect, "New name");
if ui.button("Rename").clicked() || (r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) {
if ui.button(tr("Rename")).clicked() || (r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) {
run.push(("shortcuts.preset".into(), json!({"op": "rename", "name": active, "newName": name})));
st.rename = None;
}
} else {
let b = ui.add_enabled(custom, egui::Button::new("Rename"));
let b = ui.add_enabled(custom, egui::Button::new(tr("Rename")));
app.auto.add("shortcuts.rename", b.rect, "Rename");
if b.clicked() {
st.rename = Some(active.clone());
}
}
let b = ui.add_enabled(custom, egui::Button::new("Delete"));
let b = ui.add_enabled(custom, egui::Button::new(tr("Delete")));
app.auto.add("shortcuts.delete", b.rect, "Delete");
if b.clicked() {
run.push(("shortcuts.preset".into(), json!({"op": "delete", "name": active})));
}
let b = ui.add_enabled(custom, egui::Button::new("Reset Preset"));
let b = ui.add_enabled(custom, egui::Button::new(tr("Reset Preset")));
app.auto.add("shortcuts.resetPreset", b.rect, "Reset Preset");
if b.clicked() {
run.push(("shortcuts.reset".into(), json!({})));
}
let b = ui.button("Import...");
let b = ui.button(tr("Import..."));
app.auto.add("shortcuts.import", b.rect, "Import");
if b.clicked()
&& let Some(p) = app.hooks.pick_files.as_ref().and_then(|f| f(&["json"]).into_iter().next())
{
run.push(("shortcuts.import".into(), json!({"path": p})));
}
let b = ui.button("Export...");
let b = ui.button(tr("Export..."));
app.auto.add("shortcuts.export", b.rect, "Export");
if b.clicked()
&& let Some(p) = app.hooks.pick_save_file.as_ref().and_then(|f| f(&format!("{active}.json"), "json"))
@@ -294,7 +295,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
ui.add_space(6.0);
// ---- modifiers + legend
ui.horizontal(|ui| {
ui.label("Modifiers:");
ui.label(tr("Modifiers:"));
let names = if cfg!(target_os = "macos") { ["⌃ Ctrl", "⌘ Cmd", "⌥ Opt", "⇧ Shift"] } else { ["", "Ctrl", "Alt", "Shift"] };
for (i, (n, id)) in names.iter().zip(["ctrl", "cmd", "alt", "shift"]).enumerate() {
if n.is_empty() {
@@ -396,13 +397,13 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
p.rect_filled(info, 4.0, t.row);
let p = ui.painter_at(info.shrink(2.0));
if let Some(k) = &st.key {
p.text(info.left_top() + vec2(8.0, 6.0), Align2::LEFT_TOP, format!("Key: {k}"), Tokens::semibold(12.0), t.tab_text_active);
p.text(info.left_top() + vec2(8.0, 6.0), Align2::LEFT_TOP, tr_args("Key: {}", &[&k]), Tokens::semibold(12.0), t.tab_text_active);
let mut yy = info.min.y + 26.0;
let mut list: Vec<(String, String, String)> =
table.bindings().into_iter().filter(|(sc, _)| split(sc).1 == *k).map(|(sc, b)| (sc.to_string(), b.label.clone(), b.scope.clone())).collect();
list.sort();
if list.is_empty() {
p.text(pos2(info.min.x + 8.0, yy), Align2::LEFT_TOP, "Not assigned", Tokens::ui(11.5), t.text_dim);
p.text(pos2(info.min.x + 8.0, yy), Align2::LEFT_TOP, tr("Not assigned"), Tokens::ui(11.5), t.text_dim);
}
for (sc, label, scope) in list.into_iter().take(9) {
let col = if scope == APP_SCOPE { APP_COLOR } else { PANEL_COLOR };
@@ -411,12 +412,12 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
yy += 17.0;
}
} else {
p.text(info.left_top() + vec2(8.0, 6.0), Align2::LEFT_TOP, "Click a key to see its commands.", Tokens::ui(11.5), t.text_dim);
p.text(info.left_top() + vec2(8.0, 6.0), Align2::LEFT_TOP, tr("Click a key to see its commands."), Tokens::ui(11.5), t.text_dim);
}
ui.add_space(4.0);
// ---- search + command list + editor
ui.horizontal(|ui| {
let r = ui.add(egui::TextEdit::singleline(&mut st.query).hint_text("Search commands or shortcuts").desired_width(360.0));
let r = ui.add(egui::TextEdit::singleline(&mut st.query).hint_text(tr("Search commands or shortcuts")).desired_width(360.0));
app.auto.add("shortcuts.search", r.rect, "Search");
});
ui.add_space(4.0);
@@ -476,7 +477,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
ui.vertical(|ui| {
ui.set_width(310.0);
let Some(b) = st.selected.as_ref().and_then(|k| table.find(k)).cloned() else {
ui.label(RichText::new("Select a command to change its shortcut.").color(t.text_dim));
ui.label(RichText::new(tr("Select a command to change its shortcut.")).color(t.text_dim));
return;
};
ui.label(RichText::new(&b.label).font(Tokens::semibold(13.0)).color(t.tab_text_active));
@@ -484,7 +485,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
ui.add_space(6.0);
let keys = table.keys.get(&b.key).cloned().unwrap_or_default();
if keys.is_empty() {
ui.label(RichText::new("No shortcut").color(t.text_dim));
ui.label(RichText::new(tr("No shortcut")).color(t.text_dim));
}
for (n, k) in keys.iter().enumerate() {
ui.horizontal(|ui| {
@@ -509,7 +510,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
st.recording = false;
}
let norm = normalize(&st.pending);
let b2 = ui.add_enabled(norm.is_some(), egui::Button::new("Assign"));
let b2 = ui.add_enabled(norm.is_some(), egui::Button::new(tr("Assign")));
app.auto.add("shortcuts.assign", b2.rect, "Assign");
if b2.clicked()
&& let Some(k) = norm
@@ -526,31 +527,31 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let conflicts = table.conflicts(&b, &k);
if !conflicts.is_empty() {
ui.add_space(4.0);
ui.label(RichText::new(format!("⚠ {} is used by:", crate::menus::shortcut_text(&k))).color(t.warning));
ui.label(RichText::new(tr_args("⚠ {} is used by:", &[&(crate::menus::shortcut_text(&k))])).color(t.warning));
for c in conflicts.iter().take(4) {
ui.label(RichText::new(format!(" {} ({})", c.label, c.scope)).color(t.warning));
}
}
}
ui.add_space(6.0);
let r = ui.add_enabled(keys != b.defaults, egui::Button::new("Reset to Default"));
let r = ui.add_enabled(keys != b.defaults, egui::Button::new(tr("Reset to Default")));
app.auto.add("shortcuts.resetCommand", r.rect, "Reset to Default");
if r.clicked() {
run.push(("shortcuts.reset".into(), json!({"command": b.command, "params": b.params})));
}
if !b.defaults.is_empty() {
let d: Vec<String> = b.defaults.iter().map(|k| crate::menus::shortcut_text(k)).collect();
ui.label(RichText::new(format!("Default: {}", d.join(", "))).color(t.text_faint));
ui.label(RichText::new(tr_args("Default: {}", &[&(d.join(", "))])).color(t.text_faint));
}
if active == DEFAULT_PRESET {
ui.add_space(6.0);
ui.label(RichText::new("Changing a shortcut creates a custom preset; the default stays as it is.").color(t.text_faint).small());
ui.label(RichText::new(tr("Changing a shortcut creates a custom preset; the default stays as it is.")).color(t.text_faint).small());
}
});
});
ui.add_space(6.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let r = ui.add(egui::Button::new(RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent));
let r = ui.add(egui::Button::new(RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
app.auto.add("shortcuts.close", r.rect, "OK");
close |= r.clicked();
});
+2 -2
View File
@@ -55,7 +55,7 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
let ctx = ui.ctx().clone();
let p = ui.painter().with_clip_rect(area);
let (x, y, w) = (area.min.x, area.min.y, area.width());
p.text(pos2(x, y + 6.0), Align2::LEFT_CENTER, "New from Template", Tokens::semibold(16.0), Color32::WHITE);
p.text(pos2(x, y + 6.0), Align2::LEFT_CENTER, crate::i18n::tr("New from Template"), Tokens::semibold(16.0), Color32::WHITE);
p.text(
pos2(x, y + 26.0),
Align2::LEFT_CENTER,
@@ -134,7 +134,7 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
let mut del_hit = false;
if !tpl.builtin {
let dr = Rect::from_min_size(pos2(tr.max.x - 30.0, tr.min.y + 6.0), vec2(24.0, 24.0));
let dresp = ui.interact(dr, egui::Id::new(("template-del", &tpl.id)), Sense::click()).on_hover_text("Delete this template");
let dresp = ui.interact(dr, egui::Id::new(("template-del", &tpl.id)), Sense::click()).on_hover_text(crate::i18n::tr("Delete this template"));
if resp.hovered() || dresp.hovered() {
p.circle_filled(dr.center(), 12.0, if dresp.hovered() { Color32::from_rgb(0xc0, 0x3a, 0x3a) } else { Color32::from_black_alpha(170) });
icons::paint(&p, dr.shrink(6.0), Icon::Close, Color32::WHITE);
+10 -9
View File
@@ -2,6 +2,7 @@
//! Character and Paragraph panels show and change the selected text (creating style runs);
//! otherwise they apply to the whole selected text layer.
use crate::i18n::tr;
use effectcraft_engine::keyframe::{BaselineOption, Composer, Direction, FigureStyle, FigureWidth, Justify, Kerning, TextDoc};
use effectcraft_engine::render::EvalCtx;
use egui::{Align2, Rect, Sense, pos2, vec2};
@@ -232,7 +233,7 @@ pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
actions.push(json!({"ligatures": !doc.ligatures}));
}
app.auto.add("character.ligatures", lr, "Ligatures");
p.text(pos2(x0 + 20.0, y + 7.0), Align2::LEFT_CENTER, "Ligatures", Tokens::ui(11.5), t.text_dim);
p.text(pos2(x0 + 20.0, y + 7.0), Align2::LEFT_CENTER, tr("Ligatures"), Tokens::ui(11.5), t.text_dim);
// OpenType: a popup with the font's layout features (like the Character panel's OpenType menu).
let or = Rect::from_min_size(pos2(sr.max.x + 6.0, sr.min.y), vec2(78.0, 22.0));
let opop = egui::Id::new("char-opentype-pop");
@@ -264,7 +265,7 @@ pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let axes = effectcraft_engine::text::variable::font_axes(face);
if !axes.is_empty() {
y += 26.0;
p.text(pos2(x0, y + 7.0), Align2::LEFT_CENTER, "Variable Font Axes", Tokens::semibold(11.0), t.text_dim);
p.text(pos2(x0, y + 7.0), Align2::LEFT_CENTER, tr("Variable Font Axes"), Tokens::semibold(11.0), t.text_dim);
for (i, a) in axes.iter().enumerate() {
let (col, row) = (i % 2, i / 2);
let fx = x0 + col as f32 * (w / 2.0);
@@ -287,7 +288,7 @@ pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
scroll.end(ui, &mut app.auto, "character.scroll", y + 44.0 - top, &t);
match &target {
None => {
p.text(pos2(rect.center().x, rect.max.y - 20.0), Align2::CENTER_CENTER, "Select a text layer", Tokens::ui(11.0), t.text_faint);
p.text(pos2(rect.center().x, rect.max.y - 20.0), Align2::CENTER_CENTER, tr("Select a text layer"), Tokens::ui(11.0), t.text_faint);
}
Some(tt) if tt.range.is_some() => {
let [a, b] = tt.range.unwrap_or_default();
@@ -351,7 +352,7 @@ fn opentype_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui::Id, pos
}
}
ui.separator();
ui.label(egui::RichText::new("Figures").small().color(t.text_dim));
ui.label(egui::RichText::new(tr("Figures")).small().color(t.text_dim));
let figs = [
("default", "Default Figure Style", FigureStyle::Default, FigureWidth::Default),
("tabularLining", "Tabular Lining", FigureStyle::Lining, FigureWidth::Tabular),
@@ -368,7 +369,7 @@ fn opentype_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui::Id, pos
}
}
ui.separator();
ui.label(egui::RichText::new("Stylistic Sets").small().color(t.text_dim));
ui.label(egui::RichText::new(tr("Stylistic Sets")).small().color(t.text_dim));
egui::Grid::new(id.with("sets")).spacing(vec2(3.0, 3.0)).show(ui, |ui| {
for n in 1..=20u32 {
let key = format!("ss{n:02}");
@@ -536,11 +537,11 @@ pub fn paragraph(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
actions.push(json!({"hangingPunctuation": !doc.hanging_punctuation}));
}
app.auto.add("paragraph.hangingPunctuation", hr, "Roman Hanging Punctuation");
p.text(pos2(x0 + 20.0, y + 7.0), Align2::LEFT_CENTER, "Roman Hanging Punctuation", Tokens::ui(11.5), t.text_dim);
p.text(pos2(x0 + 20.0, y + 7.0), Align2::LEFT_CENTER, tr("Roman Hanging Punctuation"), Tokens::ui(11.5), t.text_dim);
// (Room for the status line at the bottom.)
scroll.end(ui, &mut app.auto, "paragraph.scroll", y + 44.0 - top, &t);
if !enabled {
p.text(pos2(rect.center().x, rect.max.y - 20.0), Align2::CENTER_CENTER, "Select a text layer", Tokens::ui(11.0), t.text_faint);
p.text(pos2(rect.center().x, rect.max.y - 20.0), Align2::CENTER_CENTER, tr("Select a text layer"), Tokens::ui(11.0), t.text_faint);
}
let Some(tt) = target else { return };
for a in actions {
@@ -601,7 +602,7 @@ pub fn align(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let p = ui.painter().with_clip_rect(rect);
let x0 = rect.min.x + 10.0;
let mut y = rect.min.y + 10.0;
p.text(pos2(x0, y + 10.0), Align2::LEFT_CENTER, "Align Layers to:", Tokens::ui(11.5), t.text_dim);
p.text(pos2(x0, y + 10.0), Align2::LEFT_CENTER, tr("Align Layers to:"), Tokens::ui(11.5), t.text_dim);
let dr = Rect::from_min_size(pos2(x0 + 100.0, y), vec2(110.0, 20.0));
let to_sel = app.ui.align_to_selection;
let pid = egui::Id::new("align-to-pop");
@@ -619,7 +620,7 @@ pub fn align(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let mut run: Option<(&str, &str)> = None;
for (row, kind) in ["align", "distribute"].into_iter().enumerate() {
if row == 1 {
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, "Distribute Layers:", Tokens::ui(11.5), t.text_dim);
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, tr("Distribute Layers:"), Tokens::ui(11.5), t.text_dim);
y += 20.0;
}
for (i, op) in ops.iter().enumerate() {
+59 -34
View File
@@ -1499,11 +1499,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
hresp.context_menu(|ui| {
let under: &str = ctx.data(|d| d.get_temp(egui::Id::new("tl-colhdr-under"))).unwrap_or("");
if !under.is_empty() && under != "name" && ui.button("Hide This").clicked() {
if !under.is_empty() && under != "name" && ui.button(crate::i18n::tr("Hide This")).clicked() {
actions.push(("timeline.column".into(), json!({"column": under, "visible": false})));
ui.close();
}
ui.menu_button("Columns", |ui| {
ui.menu_button(crate::i18n::tr("Columns"), |ui| {
for (id, label) in COLUMNS {
let label = if id == "name" { if app.ui.timeline.source_name { "Source Name" } else { "Layer Name" } } else { label };
let on = column_visible(&app.ui.timeline, id);
@@ -1513,7 +1513,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
}
});
if ui.button(if app.ui.timeline.source_name { "Show Layer Name" } else { "Show Source Name" }).clicked() {
if ui.button(if app.ui.timeline.source_name { crate::i18n::tr("Show Layer Name") } else { crate::i18n::tr("Show Source Name") }).clicked() {
actions.push(("timeline.sourceName".into(), json!({})));
ui.close();
}
@@ -1799,8 +1799,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// Parent.
if vis.parent {
let pw_rect = Rect::from_center_size(pos2(cw.parent + 9.0, cy), vec2(15.0, 15.0));
let pw =
ui.interact(pw_rect, egui::Id::new(("parent-whip", layer.id.0)), Sense::drag()).on_hover_text("Parent pick whip: drag onto a layer");
let pw = ui
.interact(pw_rect, egui::Id::new(("parent-whip", layer.id.0)), Sense::drag())
.on_hover_text(crate::i18n::tr("Parent pick whip: drag onto a layer"));
icons::paint(&lp, pw_rect.shrink(1.0), Icon::PickWhip, if pw.hovered() || pw.dragged() { t.text } else { t.text_dim });
app.auto.add(&format!("timeline.layer.{}.pickWhip", layer.id.0), pw_rect, "Parent pick whip");
if pw.drag_started() {
@@ -1870,7 +1871,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if gr.width() < 2.0 {
continue;
}
let gresp = ui.interact(gr, egui::Id::new(("ghost", side, layer.id.0)), Sense::drag()).on_hover_text("Drag to slip the source");
let gresp = ui
.interact(gr, egui::Id::new(("ghost", side, layer.id.0)), Sense::drag())
.on_hover_text(crate::i18n::tr("Drag to slip the source"));
app.auto.add(&format!("timeline.layer.{}.source{}", layer.id.0, side), gr, "Slip");
if gresp.hovered() || gresp.dragged() {
ctx.set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
@@ -1999,9 +2002,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
text_anim_popups(app, ui, &lp, layer, *uid, cw.switches, cy, &mut actions);
shape_add_popup(app, ui, &lp, layer, *uid, cw.switches, cy, &mut actions);
if *uid == GEOM_BIT | layer.id.0 {
let lr = lp.text(pos2(cw.switches + 6.0, cy), Align2::LEFT_CENTER, "Change Renderer…", Tokens::ui(11.5), t.accent);
let resp =
ui.interact(lr, egui::Id::new(("tl-change-renderer", layer.id.0)), Sense::click()).on_hover_text("Composition Settings ▸ 3D Renderer");
let lr = lp.text(pos2(cw.switches + 6.0, cy), Align2::LEFT_CENTER, crate::i18n::tr("Change Renderer…"), Tokens::ui(11.5), t.accent);
let resp = ui
.interact(lr, egui::Id::new(("tl-change-renderer", layer.id.0)), Sense::click())
.on_hover_text(crate::i18n::tr("Composition Settings ▸ 3D Renderer"));
if resp.hovered() {
ctx.set_cursor_icon(egui::CursorIcon::PointingHand);
}
@@ -2028,7 +2032,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if widgets::checkbox(ui, ir, inverted, &t, egui::Id::new(("minv", uid))).clicked() {
actions.push(("layer.setMask".into(), json!({"layer": layer.id.0, "mask": uid, "inverted": !inverted})));
}
lp.text(pos2(ir.max.x + 4.0, cy), Align2::LEFT_CENTER, "Inverted", Tokens::ui(11.0), t.text_dim);
lp.text(pos2(ir.max.x + 4.0, cy), Align2::LEFT_CENTER, crate::i18n::tr("Inverted"), Tokens::ui(11.0), t.text_dim);
}
let gr_rect = Rect::from_min_max(pos2(indent + 8.0, r.min.y), pos2(cw.switches, r.max.y));
let gr = ui.interact(gr_rect, egui::Id::new(("grow", uid)), Sense::click());
@@ -2072,11 +2076,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let Some(ex) = prop.expr.clone() else { continue };
lp.rect_filled(left, 0.0, t.row_alt);
let indent = cw.name + 6.0 + 14.0 * row.depth as f32;
lp.text(pos2(indent + 12.0, cy), Align2::LEFT_CENTER, "Expression:", Tokens::ui(11.5), t.text_dim);
lp.text(pos2(indent + 12.0, cy), Align2::LEFT_CENTER, crate::i18n::tr("Expression:"), Tokens::ui(11.5), t.text_dim);
lp.text(pos2(indent + 82.0, cy), Align2::LEFT_CENTER, &prop.name, Tokens::ui(11.5), t.text);
// "=" enable switch and the property pick whip (AE's expression controls).
let en_r = Rect::from_center_size(pos2(cw.switches + 10.0, cy), vec2(16.0, 16.0));
let en = ui.interact(en_r, egui::Id::new(("expr-en", uid)), Sense::click()).on_hover_text("Enable Expression");
let en = ui.interact(en_r, egui::Id::new(("expr-en", uid)), Sense::click()).on_hover_text(crate::i18n::tr("Enable Expression"));
lp.rect_stroke(en_r.shrink(1.0), 2.0, Stroke::new(1.0, t.separator), StrokeKind::Inside);
let expr_col = Color32::from_rgb(0xe8, 0x7c, 0x5c);
lp.text(
@@ -2091,7 +2095,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
actions.push(("prop.setExpression".into(), json!({"layer": layer.id.0, "prop": uid, "enabled": !ex.enabled})));
}
let pw_rect = Rect::from_center_size(pos2(cw.switches + 30.0, cy), vec2(15.0, 15.0));
let pw = ui.interact(pw_rect, egui::Id::new(("expr-whip", uid)), Sense::drag()).on_hover_text("Expression pick whip: drag onto a property");
let pw = ui
.interact(pw_rect, egui::Id::new(("expr-whip", uid)), Sense::drag())
.on_hover_text(crate::i18n::tr("Expression pick whip: drag onto a property"));
icons::paint(&lp, pw_rect.shrink(1.0), Icon::PickWhip, if pw.hovered() || pw.dragged() { t.text } else { t.text_dim });
app.auto.add(&format!("timeline.prop.{uid}.pickWhip"), pw_rect, "Expression pick whip");
if pw.is_pointer_button_down_on() && ui.input(|i| i.pointer.primary_pressed()) {
@@ -2127,7 +2133,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
// ⓕ: the Expression Language menu inserts at the cursor.
let lang_r = Rect::from_center_size(pos2(cw.switches + 70.0, cy), vec2(16.0, 16.0));
let lang = ui.interact(lang_r, egui::Id::new(("expr-lang", uid)), Sense::click()).on_hover_text("Expression Language Menu");
let lang = ui.interact(lang_r, egui::Id::new(("expr-lang", uid)), Sense::click()).on_hover_text(crate::i18n::tr("Expression Language Menu"));
lp.circle_stroke(lang_r.center(), 6.5, Stroke::new(1.0, if lang.hovered() { t.text } else { t.text_dim }));
lp.text(lang_r.center(), Align2::CENTER_CENTER, "f", Tokens::semibold(10.0), if lang.hovered() { t.text } else { t.text_dim });
app.auto.add(&format!("timeline.prop.{uid}.exprLanguage"), lang_r, "Expression Language Menu");
@@ -2397,7 +2403,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
] {
if lbl == "Select All Keyframes" {
// Keyframe colour labels.
ui.menu_button("Label", |ui| {
ui.menu_button(crate::i18n::tr("Label"), |ui| {
for (i, l) in effectcraft_engine::color::Label::ALL.into_iter().enumerate() {
if widgets::label_entry(ui, l, &app.session.prefs.label_name(l), k.label as usize == i, &t).clicked() {
if !ks {
@@ -2464,7 +2470,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
for (response, area) in [(&outline_empty, "outline"), (&empty, "graph")] {
response.context_menu(|ui| {
let menu = ui.menu_button("New", |ui| {
let menu = ui.menu_button(crate::i18n::tr("New"), |ui| {
for (key, label, command, params) in [
("text", "Text", "layer.newText", json!({"text": "Text"})),
("solid", "Solid…", "layer.newSolid", json!({})),
@@ -2483,7 +2489,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
});
app.auto.add(&format!("timeline.context.{area}.new"), menu.response.rect, "New");
let response = ui.button("Composition Settings…");
let response = ui.button(crate::i18n::tr("Composition Settings…"));
app.auto.add(&format!("timeline.context.{area}.settings"), response.rect, "Composition Settings…");
if response.clicked() {
actions.push(("comp.settings".into(), json!({})));
@@ -2702,13 +2708,25 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
let tgl = Rect::from_min_size(pos2(footer.min.x + 10.0, footer.min.y + 3.0), vec2(150.0, 18.0));
let tresp = ui.interact(tgl, egui::Id::new("tl-toggle-modes"), Sense::click());
p.text(tgl.left_center(), Align2::LEFT_CENTER, "Toggle Switches / Modes", Tokens::ui(11.0), if tresp.hovered() { t.text } else { t.text_dim });
p.text(
tgl.left_center(),
Align2::LEFT_CENTER,
crate::i18n::tr("Toggle Switches / Modes"),
Tokens::ui(11.0),
if tresp.hovered() { t.text } else { t.text_dim },
);
app.auto.add("timeline.toggleSwitchesModes", tgl, "Toggle Switches / Modes");
if tresp.clicked() {
app.ui.timeline.show_modes = !app.ui.timeline.show_modes;
}
let ms = app.frames.last_ms.lock().map(|v| *v).unwrap_or(0.0);
p.text(pos2(graph_x0 - 12.0, footer.center().y), Align2::RIGHT_CENTER, format!("Frame Render Time {ms:.0}ms"), Tokens::ui(11.0), t.text_faint);
p.text(
pos2(graph_x0 - 12.0, footer.center().y),
Align2::RIGHT_CENTER,
crate::i18n::tr_args("Frame Render Time {}ms", &[&format!("{ms:.0}")]),
Tokens::ui(11.0),
t.text_faint,
);
// Zoom slider.
let zs = Rect::from_min_max(pos2(graph_x0 + 30.0, footer.center().y - 2.0), pos2((graph_x0 + 230.0).min(rect.max.x - 30.0), footer.center().y + 2.0));
icons::paint(&p, Rect::from_center_size(pos2(zs.min.x - 14.0, footer.center().y), vec2(14.0, 14.0)), Icon::MountainSmall, t.text_dim);
@@ -2896,7 +2914,7 @@ fn layer_cells(
ctx.data_mut(|d| d.insert_temp(edit_id, (el, buf)));
}
} else {
let resp = ui.interact(cr, egui::Id::new(("tl-comment", lid)), Sense::click()).on_hover_text("Double-click to edit the comment");
let resp = ui.interact(cr, egui::Id::new(("tl-comment", lid)), Sense::click()).on_hover_text(crate::i18n::tr("Double-click to edit the comment"));
lp.with_clip_rect(cr.intersect(lp.clip_rect())).text(pos2(cr.min.x + 2.0, cy), Align2::LEFT_CENTER, &layer.comment, Tokens::ui(11.5), t.text_dim);
if resp.double_clicked() {
ctx.data_mut(|d| d.insert_temp(edit_id, (lid, layer.comment.clone())));
@@ -2915,7 +2933,7 @@ fn layer_cells(
}
let cr = Rect::from_min_max(pos2(x + 2.0, r.min.y + 1.0), pos2(x + TIME_W - 2.0, r.max.y - 1.0));
let tc = crate::panels::timecode(&app.session, comp, Tick::from_seconds_f64(secs) - if kind == "duration" { comp.display_start } else { Tick(0) });
let resp = ui.interact(cr, egui::Id::new(("tl-cell", kind, lid)), Sense::drag()).on_hover_text("Drag to change");
let resp = ui.interact(cr, egui::Id::new(("tl-cell", kind, lid)), Sense::drag()).on_hover_text(crate::i18n::tr("Drag to change"));
lp.text(pos2(cr.min.x + 4.0, cy), Align2::LEFT_CENTER, &tc, Tokens::mono(11.0), if resp.hovered() || resp.dragged() { t.hot_text } else { t.timecode });
app.auto.add(&format!("timeline.layer.{lid}.{kind}"), cr, &tc);
if resp.hovered() || resp.dragged() {
@@ -2946,7 +2964,7 @@ fn layer_cells(
if cw.vis.stretch {
let cr = Rect::from_min_max(pos2(cw.stretch + 2.0, r.min.y + 1.0), pos2(cw.stretch + STRETCH_W - 2.0, r.max.y - 1.0));
let label = format!("{:.1}%", layer.stretch * 100.0);
let resp = ui.interact(cr, egui::Id::new(("tl-stretch", lid)), Sense::click()).on_hover_text("Time Stretch…");
let resp = ui.interact(cr, egui::Id::new(("tl-stretch", lid)), Sense::click()).on_hover_text(crate::i18n::tr("Time Stretch…"));
lp.text(pos2(cr.min.x + 4.0, cy), Align2::LEFT_CENTER, &label, Tokens::ui(11.5), if resp.hovered() { t.hot_text } else { t.text });
app.auto.add(&format!("timeline.layer.{lid}.stretch"), cr, &label);
if resp.clicked() {
@@ -3046,9 +3064,10 @@ fn shape_add_popup(
return;
}
let t = app.tokens;
p.text(pos2(x + 6.0, cy), Align2::LEFT_CENTER, "Add:", Tokens::ui(11.5), t.text_dim);
p.text(pos2(x + 6.0, cy), Align2::LEFT_CENTER, crate::i18n::tr("Add:"), Tokens::ui(11.5), t.text_dim);
let br = Rect::from_center_size(pos2(x + 40.0, cy), vec2(16.0, 16.0));
let resp = ui.interact(br, egui::Id::new(("tl-shapeadd", uid)), Sense::click()).on_hover_text("Add a shape, fill, stroke or path operation");
let resp =
ui.interact(br, egui::Id::new(("tl-shapeadd", uid)), Sense::click()).on_hover_text(crate::i18n::tr("Add a shape, fill, stroke or path operation"));
icons::paint(p, br.shrink(3.0), Icon::ChevronRight, if resp.hovered() { t.text } else { t.text_dim });
app.auto.add(&format!("timeline.group.{uid}.add"), br, "Add:");
let pop = egui::Id::new(("tl-shapeadd-pop", uid));
@@ -3179,7 +3198,7 @@ pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], actions: &mut Vec<(S
ui.close();
}
};
ui.menu_button("New", |ui| {
ui.menu_button(crate::i18n::tr("New"), |ui| {
item(ui, "Text", "layer.newText", json!({"text": "Text"}));
item(ui, "Solid…", "app.solidSettings", json!({}));
item(ui, "Light…", "layer.newLight", json!({}));
@@ -3190,24 +3209,24 @@ pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], actions: &mut Vec<(S
});
item(ui, "Layer Settings…", "layer.settings", json!({}));
ui.separator();
ui.menu_button("Masks", |ui| {
ui.menu_button(crate::i18n::tr("Masks"), |ui| {
item(ui, "New Mask", "layer.addMask", json!({"layer": id}));
});
ui.menu_button("Transform", |ui| {
ui.menu_button(crate::i18n::tr("Transform"), |ui| {
item(ui, "Reset", "layer.transform", json!({"layers": layers, "op": "reset"}));
item(ui, "Center In View", "layer.transform", json!({"layers": layers, "op": "center"}));
item(ui, "Fit to Comp", "layer.transform", json!({"layers": layers, "op": "fit"}));
item(ui, "Flip Horizontal", "layer.transform", json!({"layers": layers, "op": "flipH"}));
item(ui, "Flip Vertical", "layer.transform", json!({"layers": layers, "op": "flipV"}));
});
ui.menu_button("Time", |ui| {
ui.menu_button(crate::i18n::tr("Time"), |ui| {
item(ui, "Enable Time Remapping", "layer.enableTimeRemap", json!({"layers": layers}));
item(ui, "Time-Reverse Layer", "layer.timeReverse", json!({"layers": layers}));
item(ui, "Time Stretch…", "layer.timeStretch", json!({}));
item(ui, "Freeze Frame", "layer.freezeFrame", json!({"layers": layers}));
item(ui, "Freeze on Last Frame", "layer.freezeOnLastFrame", json!({"layers": layers}));
});
ui.menu_button("Blending Mode", |ui| {
ui.menu_button(crate::i18n::tr("Blending Mode"), |ui| {
widgets::menu_scroll(ui, |ui| {
for m in BlendMode::ALL {
item(ui, m.label(), "layer.setBlendMode", json!({"layers": layers, "mode": m.label()}));
@@ -3217,7 +3236,7 @@ pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], actions: &mut Vec<(S
}
})
});
ui.menu_button("Arrange", |ui| {
ui.menu_button(crate::i18n::tr("Arrange"), |ui| {
item(ui, "Bring Layer to Front", "layer.arrange", json!({"layers": layers, "to": "front"}));
item(ui, "Bring Layer Forward", "layer.arrange", json!({"layers": layers, "to": "forward"}));
item(ui, "Send Layer Backward", "layer.arrange", json!({"layers": layers, "to": "backward"}));
@@ -3309,7 +3328,7 @@ fn value_editor(
let linked = linkable && !app.ui.timeline.unlinked.contains(&uid);
if linkable {
let lr = Rect::from_center_size(pos2(x + 7.0, at.y), vec2(14.0, 14.0));
let resp = ui.interact(lr, egui::Id::new(("tl-link", uid)), Sense::click()).on_hover_text("Constrain Proportions");
let resp = ui.interact(lr, egui::Id::new(("tl-link", uid)), Sense::click()).on_hover_text(crate::i18n::tr("Constrain Proportions"));
icons::paint(p, lr, Icon::Link, if linked { t.accent } else { t.text_faint });
app.auto.add(&format!("timeline.prop.{uid}.link"), lr, "Constrain Proportions");
if resp.clicked() && !app.ui.timeline.unlinked.remove(&uid) {
@@ -3384,14 +3403,20 @@ fn value_editor(
}
}
Value::Path(_) => {
p.text(pos2(x, at.y), Align2::LEFT_CENTER, "Shape…", Tokens::ui(12.0), t.hot_text);
p.text(pos2(x, at.y), Align2::LEFT_CENTER, crate::i18n::tr("Shape…"), Tokens::ui(12.0), t.hot_text);
}
Value::Text(_) => {}
Value::Layer(l) => {
p.text(pos2(x, at.y), Align2::LEFT_CENTER, l.map(|l| format!("Layer {l}")).unwrap_or("None".into()), Tokens::ui(12.0), t.hot_text);
p.text(
pos2(x, at.y),
Align2::LEFT_CENTER,
l.map(|l| crate::i18n::tr_args("Layer {}", &[&l])).unwrap_or(crate::i18n::tr("None").into()),
Tokens::ui(12.0),
t.hot_text,
);
}
Value::Gradient(_) => {
p.text(pos2(x, at.y), Align2::LEFT_CENTER, "Edit Gradient…", Tokens::ui(12.0), t.hot_text);
p.text(pos2(x, at.y), Align2::LEFT_CENTER, crate::i18n::tr("Edit Gradient…"), Tokens::ui(12.0), t.hot_text);
}
Value::Str(s) => {
p.text(pos2(x, at.y), Align2::LEFT_CENTER, s.chars().take(30).collect::<String>(), Tokens::ui(12.0), t.text_dim);
+16 -16
View File
@@ -153,7 +153,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
y += 34.0;
let Some(comp) = app.session.active_comp_arc() else {
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, "Open a composition to track motion.", Tokens::ui(12.0), t.text_faint);
p.text(pos2(x0, y + 8.0), Align2::LEFT_CENTER, crate::i18n::tr("Open a composition to track motion."), Tokens::ui(12.0), t.text_faint);
return;
};
// A selected mask switches the panel to mask tracking.
@@ -476,10 +476,10 @@ fn ok_cancel(ui: &mut egui::Ui, app: &mut EffectcraftApp, t: &Tokens, prefix: &s
let (mut ok, mut cancel) = (false, false);
ui.add_space(14.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let r = ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent));
let r = ui.add(egui::Button::new(egui::RichText::new(crate::i18n::tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
app.auto.add(&format!("{prefix}.ok"), r.rect, "OK");
ok = r.clicked();
let r = ui.button("Cancel");
let r = ui.button(crate::i18n::tr("Cancel"));
app.auto.add(&format!("{prefix}.cancel"), r.rect, "Cancel");
cancel = r.clicked();
});
@@ -491,12 +491,12 @@ pub fn options_dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens)
let (mut ok, mut cancel) = (false, false);
super::dialogs::modal(ctx, "Motion Tracker Options", vec2(460.0, 470.0), t, |ui| {
ui.horizontal(|ui| {
ui.label("Track Name:");
ui.label(crate::i18n::tr("Track Name:"));
let r = ui.text_edit_singleline(&mut d.name);
app.auto.add("dialog.trackOptions.name", r.rect, "Track Name");
});
ui.add_space(8.0);
ui.label(egui::RichText::new("Channel").font(Tokens::medium(12.5)));
ui.label(egui::RichText::new(crate::i18n::tr("Channel")).font(Tokens::medium(12.5)));
ui.horizontal(|ui| {
for (c, l) in [(TrackChannel::Rgb, "RGB"), (TrackChannel::Luminance, "Luminance"), (TrackChannel::Saturation, "Saturation")] {
let r = ui.radio_value(&mut d.opts.channel, c, l);
@@ -504,21 +504,21 @@ pub fn options_dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens)
}
});
ui.add_space(8.0);
ui.label(egui::RichText::new("Process Before Match").font(Tokens::medium(12.5)));
ui.label(egui::RichText::new(crate::i18n::tr("Process Before Match")).font(Tokens::medium(12.5)));
ui.horizontal(|ui| {
let r = ui.checkbox(&mut d.blur_on, "Blur");
let r = ui.checkbox(&mut d.blur_on, crate::i18n::tr("Blur"));
app.auto.add("dialog.trackOptions.blurOn", r.rect, "Blur");
let r = ui.add_enabled(d.blur_on, egui::DragValue::new(&mut d.opts.blur).speed(0.1).range(0.5..=50.0).suffix(" pixels"));
app.auto.add("dialog.trackOptions.blur", r.rect, "Blur pixels");
let r = ui.checkbox(&mut d.opts.enhance, "Enhance");
let r = ui.checkbox(&mut d.opts.enhance, crate::i18n::tr("Enhance"));
app.auto.add("dialog.trackOptions.enhance", r.rect, "Enhance");
});
ui.add_space(8.0);
let r = ui.checkbox(&mut d.opts.subpixel, "Subpixel Positioning");
let r = ui.checkbox(&mut d.opts.subpixel, crate::i18n::tr("Subpixel Positioning"));
app.auto.add("dialog.trackOptions.subpixel", r.rect, "Subpixel Positioning");
let r = ui.checkbox(&mut d.opts.adapt_every_frame, "Adapt Feature On Every Frame");
let r = ui.checkbox(&mut d.opts.adapt_every_frame, crate::i18n::tr("Adapt Feature On Every Frame"));
app.auto.add("dialog.trackOptions.adaptEveryFrame", r.rect, "Adapt Feature On Every Frame");
let r = ui.checkbox(&mut d.opts.track_shape, "Follow Feature Rotation and Scale");
let r = ui.checkbox(&mut d.opts.track_shape, crate::i18n::tr("Follow Feature Rotation and Scale"));
app.auto.add("dialog.trackOptions.trackShape", r.rect, "Follow Feature Rotation and Scale");
ui.add_space(8.0);
ui.horizontal(|ui| {
@@ -538,7 +538,7 @@ pub fn options_dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens)
})
.response;
app.auto.add("dialog.trackOptions.action", r.rect, "If Confidence is Below action");
ui.label("If Confidence is Below");
ui.label(crate::i18n::tr("If Confidence is Below"));
let r = ui.add(egui::DragValue::new(&mut d.opts.threshold).speed(0.5).range(0.0..=100.0).suffix(" %"));
app.auto.add("dialog.trackOptions.threshold", r.rect, "Confidence threshold");
});
@@ -578,15 +578,15 @@ pub fn target_dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens)
let layers: Vec<(u64, String)> =
app.session.active_comp().map(|c| c.layers.iter().enumerate().map(|(i, l)| (l.id.0, format!("{}. {}", i + 1, l.name))).collect()).unwrap_or_default();
super::dialogs::modal(ctx, "Motion Target", vec2(400.0, 200.0), t, |ui| {
ui.label("Apply Motion To:");
ui.label(crate::i18n::tr("Apply Motion To:"));
ui.horizontal(|ui| {
ui.label("Layer:");
ui.label(crate::i18n::tr("Layer:"));
let cur = d.target.and_then(|id| layers.iter().find(|(l, _)| *l == id)).map(|(_, n)| n.clone()).unwrap_or_else(|| "None".into());
let r = egui::ComboBox::from_id_salt("track-target")
.selected_text(cur)
.width(240.0)
.show_ui(ui, |ui| {
ui.selectable_value(&mut d.target, None, "None");
ui.selectable_value(&mut d.target, None, crate::i18n::tr("None"));
for (id, n) in &layers {
ui.selectable_value(&mut d.target, Some(*id), n);
}
@@ -613,7 +613,7 @@ pub fn apply_dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let (mut ok, mut cancel) = (false, false);
super::dialogs::modal(ctx, "Motion Tracker Apply Options", vec2(380.0, 170.0), t, |ui| {
ui.horizontal(|ui| {
ui.label("Apply Dimensions:");
ui.label(crate::i18n::tr("Apply Dimensions:"));
let labels = ["X and Y", "X only", "Y only"];
let r = egui::ComboBox::from_id_salt("track-apply-dims")
.selected_text(labels[d.dims.min(2)])
+4 -3
View File
@@ -7,6 +7,7 @@
//! Automation ids: `dialog.unsaved.save`, `dialog.unsaved.dontSave` (Revert: `dialog.unsaved.revert`),
//! `dialog.unsaved.cancel`.
use crate::i18n::tr;
use egui::{Color32, vec2};
use serde_json::{Value, json};
@@ -96,7 +97,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
ui.label(egui::RichText::new(text).color(t.tab_text_active));
if !revert {
ui.add_space(4.0);
ui.label(egui::RichText::new("Your changes will be lost if you don't save them.").color(t.text_dim).size(11.0));
ui.label(egui::RichText::new(tr("Your changes will be lost if you don't save them.")).color(t.text_dim).size(11.0));
}
ui.add_space(18.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
@@ -106,7 +107,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
if r.clicked() {
answer = Some(if revert { Answer::Discard } else { Answer::Save });
}
let r = ui.button("Cancel");
let r = ui.button(tr("Cancel"));
app.auto.add("dialog.unsaved.cancel", r.rect, "Cancel");
if r.clicked() {
answer = Some(Answer::Cancel);
@@ -114,7 +115,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
if !revert {
// Set apart from Save, as is customary.
ui.add_space(ui.available_width() - 90.0);
let r = ui.button("Don't Save");
let r = ui.button(tr("Don't Save"));
app.auto.add("dialog.unsaved.dontSave", r.rect, "Don't Save");
if r.clicked() {
answer = Some(Answer::Discard);
+7 -7
View File
@@ -466,7 +466,7 @@ fn aux_views(
p.rect_stroke(Rect::from_center_size(cr.center(), cr.size() * k), 0.0, Stroke::new(1.0, Color32::from_white_alpha(120)), StrokeKind::Middle);
}
}
let label = if comp.has_3d() { v.label() } else { "Active Camera" };
let label = if comp.has_3d() { v.label() } else { crate::i18n::tr("Active Camera") };
p.text(r.left_bottom() + vec2(8.0, -8.0), Align2::LEFT_BOTTOM, label, Tokens::ui(11.0), Color32::from_white_alpha(200));
app.auto.add(&format!("viewer.view.{}", v.id()), r, label);
}
@@ -523,7 +523,7 @@ fn locked_pane(app: &mut EffectcraftApp, ui: &mut egui::Ui, full: Rect, bg: Colo
p.rect_filled(cr, 0.0, Color32::from_rgb((b[0] * 255.0) as u8, (b[1] * 255.0) as u8, (b[2] * 255.0) as u8));
p.image(tex.id(), cr, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
p.rect_stroke(cr, 0.0, Stroke::new(1.0, Color32::from_black_alpha(160)), StrokeKind::Outside);
let label = format!("Locked: {name} \u{2014} {}", if comp.has_3d() { lv.view.label() } else { "Active Camera" });
let label = crate::i18n::tr_args("Locked: {} — {}", &[&name, &(if comp.has_3d() { lv.view.label() } else { crate::i18n::tr("Active Camera") })]);
p.text(r.left_top() + vec2(8.0, 8.0), Align2::LEFT_TOP, &label, Tokens::ui(11.0), Color32::from_white_alpha(210));
app.auto.add("viewer.locked", r, &label);
// Close the locked viewer.
@@ -986,7 +986,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
resp.context_menu(|ui| {
let hits = ui.data(|d| d.get_temp::<Vec<(u64, String)>>(menu_hits_id)).unwrap_or_default();
if !hits.is_empty() {
ui.menu_button("Select", |ui| {
ui.menu_button(crate::i18n::tr("Select"), |ui| {
crate::widgets::menu_scroll(ui, |ui| {
for (id, name) in &hits {
if ui.button(name).clicked() {
@@ -999,7 +999,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
ui.separator();
}
if context_layers.is_empty() {
ui.label("No unlocked layer selected");
ui.label(crate::i18n::tr("No unlocked layer selected"));
} else {
super::timeline::layer_menu(ui, &context_layers, &mut context_actions);
}
@@ -1403,7 +1403,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let speed = app.session.state.puppet.record_speed.max(1.0) / 100.0;
app.session.set_time(cti + Tick::from_seconds_f64(now / speed));
painter.circle_filled(pos, 6.0, Color32::from_rgb(0xe0, 0x30, 0x30));
painter.text(pos + vec2(10.0, -10.0), Align2::LEFT_BOTTOM, "Recording", Tokens::ui(11.0), Color32::from_rgb(0xff, 0x60, 0x60));
painter.text(pos + vec2(10.0, -10.0), Align2::LEFT_BOTTOM, crate::i18n::tr("Recording"), Tokens::ui(11.0), Color32::from_rgb(0xff, 0x60, 0x60));
ui.data_mut(|dd| dd.insert_temp(gid, Gesture::PuppetRecord { layer, pin, others, inv, began, cti, samples }));
ui.ctx().request_repaint();
}
@@ -1644,7 +1644,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if app.playback.playing && app.playback.waiting {
let r = Rect::from_min_size(area.min + vec2(10.0, 10.0), vec2(150.0, 22.0));
painter.rect_filled(r, 11.0, Color32::from_black_alpha(170));
painter.text(r.center(), Align2::CENTER_CENTER, "Caching frames…", Tokens::ui(11.5), t.cache_green);
painter.text(r.center(), Align2::CENTER_CENTER, crate::i18n::tr("Caching frames…"), Tokens::ui(11.5), t.cache_green);
}
vt::draw_snap(&ctx, &painter, &map, &ectx);
@@ -1928,7 +1928,7 @@ fn empty_state(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let p = ui.painter();
p.rect_filled(rect, 0.0, t.pasteboard);
let c = rect.center();
p.text(c - vec2(0.0, 40.0), Align2::CENTER_CENTER, "Composition", Tokens::semibold(16.0), t.text);
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() {
crate::panels::dialogs::open_new_comp(app);
+12 -8
View File
@@ -225,8 +225,9 @@ pub(crate) fn rulers(app: &mut EffectcraftApp, ui: &mut egui::Ui, map: &ViewerMa
// Zero point: drag the corner, double-click to reset.
p.line_segment([corner.center() - vec2(4.0, 0.0), corner.center() + vec2(4.0, 0.0)], tick);
p.line_segment([corner.center() - vec2(0.0, 4.0), corner.center() + vec2(0.0, 4.0)], tick);
let cr =
ui.interact(corner, egui::Id::new("vw-ruler-origin"), Sense::click_and_drag()).on_hover_text("Ruler zero point (drag to move, double-click to reset)");
let cr = ui
.interact(corner, egui::Id::new("vw-ruler-origin"), Sense::click_and_drag())
.on_hover_text(crate::i18n::tr("Ruler zero point (drag to move, double-click to reset)"));
app.auto.add("viewer.ruler.origin", corner, "Ruler zero point");
let fg = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Foreground, egui::Id::new("vw-ruler-fg")));
if cr.dragged()
@@ -247,7 +248,7 @@ pub(crate) fn rulers(app: &mut EffectcraftApp, ui: &mut egui::Ui, map: &ViewerMa
// Guides dragged out of the rulers: the top ruler makes horizontal guides, the left one
// vertical guides.
for (r, vertical, id) in [(top, false, "top"), (left, true, "left")] {
let resp = ui.interact(r, egui::Id::new(("vw-ruler", id)), Sense::drag()).on_hover_text("Drag to create a guide");
let resp = ui.interact(r, egui::Id::new(("vw-ruler", id)), Sense::drag()).on_hover_text(crate::i18n::tr("Drag to create a guide"));
app.auto.add(&format!("viewer.ruler.{id}"), r, if vertical { "Vertical ruler" } else { "Horizontal ruler" });
if resp.hovered() {
ui.ctx().set_cursor_icon(if vertical { egui::CursorIcon::ResizeHorizontal } else { egui::CursorIcon::ResizeVertical });
@@ -356,7 +357,7 @@ pub(crate) fn draw_frame(app: &mut EffectcraftApp, ctx: &egui::Context, painter:
let snap = showing_snapshot(app, ctx);
// Settings ▸ Video ▸ Mirror on Computer Monitor off: playback goes to Video Preview only.
if crate::prefs_live::main_viewer_hidden(app) && !snap {
painter.text(comp_rect.center(), Align2::CENTER_CENTER, "Playing on Video Preview", Tokens::ui(13.0), Color32::from_gray(150));
painter.text(comp_rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("Playing on Video Preview"), Tokens::ui(13.0), Color32::from_gray(150));
return;
}
if opts.fast_previews == FastPreviews::Wireframe && !snap {
@@ -403,7 +404,7 @@ pub(crate) fn draw_frame(app: &mut EffectcraftApp, ctx: &egui::Context, painter:
}
};
painter.image(tex.id(), comp_rect, uv, Color32::WHITE);
painter.text(comp_rect.left_top() + vec2(6.0, 6.0), Align2::LEFT_TOP, "Snapshot", Tokens::ui(11.0), SNAP_COLOR);
painter.text(comp_rect.left_top() + vec2(6.0, 6.0), Align2::LEFT_TOP, crate::i18n::tr("Snapshot"), Tokens::ui(11.0), SNAP_COLOR);
return;
}
// A GPU frame (the web viewer): drawn straight from its texture; Show Channel / exposure
@@ -530,7 +531,10 @@ pub(crate) fn bottom_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect,
// Magnification.
let r = Rect::from_min_size(pos2(x, cy - 10.0), vec2(80.0, 20.0));
if widgets::dropdown(ui, r, &magnification_label(zoom * ppp * 100.0), &t, egui::Id::new("vw-mag")).on_hover_text("Magnification ratio popup").clicked() {
if widgets::dropdown(ui, r, &magnification_label(zoom * ppp * 100.0), &t, egui::Id::new("vw-mag"))
.on_hover_text(crate::i18n::tr("Magnification ratio popup"))
.clicked()
{
toggle_popup(ui, "vw-mag-pop");
}
app.auto.add("viewer.magnification", r, "Magnification");
@@ -560,7 +564,7 @@ pub(crate) fn bottom_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect,
let r = Rect::from_min_size(pos2(x, cy - 10.0), vec2(84.0, 20.0));
let scale = app.viewer_scale(zoom, ppp);
if widgets::dropdown(ui, r, &resolution_label(app.ui.viewer.res, scale), &t, egui::Id::new("vw-res"))
.on_hover_text("Resolution/Down Sample Factor Popup")
.on_hover_text(crate::i18n::tr("Resolution/Down Sample Factor Popup"))
.clicked()
{
toggle_popup(ui, "vw-res-pop");
@@ -666,7 +670,7 @@ pub(crate) fn bottom_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect,
.fixed_decimals(1)
.custom_formatter(|v, _| if v == 0.0 { "0.0".into() } else { format!("{v:+.1}") }),
);
let resp = resp.on_hover_text("Adjust Exposure (stops)");
let resp = resp.on_hover_text(crate::i18n::tr("Adjust Exposure (stops)"));
app.auto.add("viewer.exposure", er, "Adjust Exposure");
if resp.changed() {
let _ = app.session.execute("view.exposure", json!({"stops": ev}));
+3 -2
View File
@@ -6,6 +6,7 @@
//! the active viewer is locked shows it in an unlocked viewer, or in a new one if every viewer is
//! locked.
use crate::i18n::tr;
use std::sync::Arc;
use effectcraft_engine::project::ItemId;
@@ -88,7 +89,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: u32, rect: Rect) {
let ctx = ui.ctx().clone();
let p = ui.painter().with_clip_rect(rect);
p.rect_filled(rect, 0.0, t.pasteboard);
let resp = ui.interact(rect, egui::Id::new(("passive-viewer", id)), Sense::click()).on_hover_text("Click to make this viewer active");
let resp = ui.interact(rect, egui::Id::new(("passive-viewer", id)), Sense::click()).on_hover_text(tr("Click to make this viewer active"));
app.auto.add(&format!("viewers.{id}"), rect, "Composition viewer (click to make it active)");
match comp_of(app, id).and_then(|cid| Some((cid, app.session.project.comp(cid)?.clone()))) {
Some((cid, c)) => {
@@ -116,7 +117,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: u32, rect: Rect) {
p.rect_stroke(r, 0.0, egui::Stroke::new(1.0, t.separator), egui::StrokeKind::Outside);
}
None => {
p.text(rect.center(), Align2::CENTER_CENTER, "Click to show the active composition here", Tokens::ui(12.0), t.text_faint);
p.text(rect.center(), Align2::CENTER_CENTER, tr("Click to show the active composition here"), Tokens::ui(12.0), t.text_faint);
}
}
if resp.clicked() {
+1 -1
View File
@@ -34,7 +34,7 @@ of them have been reconciled.
| Behaves like After Effects | largely unmeasured, ≈ 60–80% | One outside contributor found four bugs in features marked done within a day (PRs #6–#10): Hold keyframes eased the motion into them, `keyInSpatialTangent` / `keyOutSpatialTangent` had the wrong names, a zero frame rate crashed, Find and Enter Full Screen shared Ctrl+F off macOS. There are about 10 After Effects reference captures in total. The first live G1 comparison now checks eight scalar Rotation/ease cases (168 samples) against AE 26.3x87 via AEsync 2.0.4: all pass after fixing overlapping temporal influences, with maximum error below 3e-11 degrees ([scores and reproduction](fidelity/README.md)). Other animation behavior and rendered frames remain unmeasured. User reports of 6 October found five more in features marked done (the Render Queue's template menu, angle revolutions, switches wiping the RAM preview, audio playback skipping frames, preview crashing when video memory ran out; all fixed 7 October) |
| Opening existing After Effects work | ≈ 0% | `.aep` / `.aepx` projects cannot be opened. Third-party After Effects plug-ins cannot run. Expressions and the scripting object model are strong, so scripts and expressions carry over |
| Stability | improving | 23 never-crash PRs landed on 4 October; community PRs on 7 October fixed a hang on projects whose parent chains loop (#142), a panic on non-ASCII label colours (#135), and contained GPU initialization and device failures (#139); [AGENTS.md](../AGENTS.md) "Never crash" now binds every crate. On 5 October `cargo xtask ci` failed on main under Rust 1.99's clippy (a fix is in progress) |
| Real-user experience | ≈ 70%, uneven by platform | Issues #41–#47 (all from the Linux AppImage 0.1.1, 5 October): panning the viewer snaps back, panels can't be resized or rearranged, drag-and-drop and double-click import don't work, layer rename gets stuck, the Layer Settings arrow does nothing, the Project panel clips, the Wayland window icon is generic. Issues #63–#68 (macOS, 5 October): scaling a layer by its handles goes wrong and can stick at 0, two 3D compasses, the viewer lags while a layer is dragged (a frame renders in 20 ms but showed after ≈ 100 ms, behind RAM preview prefetch), the Discord and other links do nothing, Delete doesn't delete in the Project panel, hidden layers can be selected in the viewer. All but the last two were fixed or confirmed fixed with a test on 5–6 October (those two have community PRs); file drops still can't work on Wayland (winit has no support). The panning, rename and arrow bugs had been fixed in v0.2.0 already; nobody had told the reporter. A Windows user (6 October, on Discord) found the font menus listed only the three bundled fonts: installed fonts were read only when a project asked for one, on every platform. Fixed with a regression test on 6 October; the menus now list every installed family and its own styles, and agents get `text.fonts` / `list_fonts`. Seven reports of 6 October (Linux .deb and Windows): Project items and files couldn't be dropped on the viewer (#85), nor effects on a layer there (#88); drops in the Timeline ignored where they landed (#89); an angle's revolutions couldn't be edited (#93); toggling Audio, Lock or Shy threw away the RAM preview and playback with audio skipped frames (#103); running out of video memory panicked every frame thread and left frames stuck (#106); choosing a Render Settings template in the Render Queue did nothing (#117: any popup menu moved up to fit the window closed on the press). All seven were fixed with regression tests on 7 October. Reports of 7 October (Linux, macOS, Windows): the app opened the demo project on every launch, so a new comp landed in it (#204); a comp wider than the GPU's texture limit (11000 × 2200) at Full resolution stopped the window drawing (#201); an Intel HD Graphics 5500 couldn't start the app (#198: most likely the window's device asked for limits that GPU doesn't have, and the failure was silent); Audio Spectrum / Waveform stood still in the viewer (#209) and preview sound waited for the whole work area to cache (#208); a layer couldn't be deselected from the time graph, a shape layer's Fill and Stroke showed only with a shape tool and a drawn shape had no stroke to change, Alt+Shift+P didn't reveal Position (#205); a mask selected in the Timeline wasn't selected in the viewer, Mask Feather had no link and took negative values, the Project panel had no selection box (#203); shape layers had no Add menu for Trim Paths and the other operations, and Classic 3D comps hid Geometry Options (#206); dragging Puppet pins re-rendered the layer under the Puppet at every move (#212). All were fixed with regression tests the same day (#219–#222); the #198 fix couldn't be checked on that GPU. Reports of 8 October (browser, Windows, macOS): an empty project at 1280 × 800 with device pixel ratio 2 crashed on its first frame, because the Project panel's scroll bar clamped its thumb to a track shorter than the thumb (#231, any short panel on every platform); Media Browser ▸ Add Files… dropped a file it couldn't read without a word (#226); after the page's WebGPU device was lost, the browser editor froze on its last frame while edits went on (#225); a press in the viewer always took the topmost layer, so a selected layer behind others couldn't be dragged (#230); the language didn't follow the system's (#229); and a list of missing After Effects behaviour (#227): drawing into, copying, pasting and duplicating shape contents, Tool Creates Shape / Mask and Fill / Stroke Options, Turbulent Displace, Wave Warp and Bulge pinning all but a shape layer's bottom-right quadrant, a Spacebar hold that previewed instead of being the Hand tool, the Effect menu in Effect Controls, dropping items on New Composition and dragging over layer switches. All were fixed with regression tests the same day (#235, #237–#241); multi-monitor support and the Project panel's empty-area menu (community PR #210) remain from #227, and effect points on shape and text layers still sit half a comp off. A list of tweaks from an Intel iMac (#290): the layer bar didn't show where extended footage runs past its source, and Enable Time Remapping then ended at the extended out point; footage items had no Interpret Footage in their menu and the work area bar no menu; Label menus named the labels without their colours; the arrow keys didn't nudge layers; a straight motion path had no Bezier handles; Delete on Time Remap deleted the layer; a mask being drawn with the Pen blacked out its layer until it was closed; a doubleLine truncated
| Real-user experience | ≈ 70%, uneven by platform | Issues #41–#47 (all from the Linux AppImage 0.1.1, 5 October): panning the viewer snaps back, panels can't be resized or rearranged, drag-and-drop and double-click import don't work, layer rename gets stuck, the Layer Settings arrow does nothing, the Project panel clips, the Wayland window icon is generic. Issues #63–#68 (macOS, 5 October): scaling a layer by its handles goes wrong and can stick at 0, two 3D compasses, the viewer lags while a layer is dragged (a frame renders in 20 ms but showed after ≈ 100 ms, behind RAM preview prefetch), the Discord and other links do nothing, Delete doesn't delete in the Project panel, hidden layers can be selected in the viewer. All but the last two were fixed or confirmed fixed with a test on 5–6 October (those two have community PRs); file drops still can't work on Wayland (winit has no support). The panning, rename and arrow bugs had been fixed in v0.2.0 already; nobody had told the reporter. A Windows user (6 October, on Discord) found the font menus listed only the three bundled fonts: installed fonts were read only when a project asked for one, on every platform. Fixed with a regression test on 6 October; the menus now list every installed family and its own styles, and agents get `text.fonts` / `list_fonts`. Seven reports of 6 October (Linux .deb and Windows): Project items and files couldn't be dropped on the viewer (#85), nor effects on a layer there (#88); drops in the Timeline ignored where they landed (#89); an angle's revolutions couldn't be edited (#93); toggling Audio, Lock or Shy threw away the RAM preview and playback with audio skipped frames (#103); running out of video memory panicked every frame thread and left frames stuck (#106); choosing a Render Settings template in the Render Queue did nothing (#117: any popup menu moved up to fit the window closed on the press). All seven were fixed with regression tests on 7 October. Reports of 7 October (Linux, macOS, Windows): the app opened the demo project on every launch, so a new comp landed in it (#204); a comp wider than the GPU's texture limit (11000 × 2200) at Full resolution stopped the window drawing (#201); an Intel HD Graphics 5500 couldn't start the app (#198: most likely the window's device asked for limits that GPU doesn't have, and the failure was silent); Audio Spectrum / Waveform stood still in the viewer (#209) and preview sound waited for the whole work area to cache (#208); a layer couldn't be deselected from the time graph, a shape layer's Fill and Stroke showed only with a shape tool and a drawn shape had no stroke to change, Alt+Shift+P didn't reveal Position (#205); a mask selected in the Timeline wasn't selected in the viewer, Mask Feather had no link and took negative values, the Project panel had no selection box (#203); shape layers had no Add menu for Trim Paths and the other operations, and Classic 3D comps hid Geometry Options (#206); dragging Puppet pins re-rendered the layer under the Puppet at every move (#212). All were fixed with regression tests the same day (#219–#222); the #198 fix couldn't be checked on that GPU. Reports of 8 October (browser, Windows, macOS): an empty project at 1280 × 800 with device pixel ratio 2 crashed on its first frame, because the Project panel's scroll bar clamped its thumb to a track shorter than the thumb (#231, any short panel on every platform); Media Browser ▸ Add Files… dropped a file it couldn't read without a word (#226); after the page's WebGPU device was lost, the browser editor froze on its last frame while edits went on (#225); a press in the viewer always took the topmost layer, so a selected layer behind others couldn't be dragged (#230); the language didn't follow the system's (#229); and a list of missing After Effects behaviour (#227): drawing into, copying, pasting and duplicating shape contents, Tool Creates Shape / Mask and Fill / Stroke Options, Turbulent Displace, Wave Warp and Bulge pinning all but a shape layer's bottom-right quadrant, a Spacebar hold that previewed instead of being the Hand tool, the Effect menu in Effect Controls, dropping items on New Composition and dragging over layer switches. All were fixed with regression tests the same day (#235, #237–#241); multi-monitor support and the Project panel's empty-area menu (community PR #210) remain from #227, and effect points on shape and text layers still sit half a comp off. A list of tweaks from an Intel iMac (#290): the layer bar didn't show where extended footage runs past its source, and Enable Time Remapping then ended at the extended out point; footage items had no Interpret Footage in their menu and the work area bar no menu; Label menus named the labels without their colours; the arrow keys didn't nudge layers; a straight motion path had no Bezier handles; Delete on Time Remap deleted the layer; a mask being drawn with the Pen blacked out its layer until it was closed; a doubleLine truncated
| Performance | unknown against After Effects | Internal numbers only (e.g. Advanced 3D 290 ms/frame at 1080p on the GPU, an M4 Pro under load). Nothing benchmarked against After Effects; no large real projects (4K footage, hundreds of layers) tested |
| Media formats | ≈ 80% | H.264, ProRes, HEVC, AV1, image sequences and audio exist. The new HEVC / AV1 encoders have no B-frames, multi-reference or SAO / CDEF, so files are larger than from mature encoders. Camera formats (BRAW, R3D, ProRes RAW, variable-frame-rate phone video) are unverified |
| AI-assisted tools | ≈ 50% | Both tools can use trained models (pure-Rust inference, optional downloads), but nothing compares them with After Effects yet. Roto Brush 2.0 / 3.0 with MobileSAM (M13.35) scores IoU 0.989 on the base frame of our synthetic moving-disc test and ≥ 0.980 over 20 propagated frames (classic: 0.973). Face tracking with MediaPipe Face Landmarker (M13.36) matches Google's own pipeline to 0.85 px on average on a test portrait; on our synthetic clip the eyes and chin stay within 4% of the face height |
+1 -1
View File
@@ -29,7 +29,7 @@ Commands (CLI, MCP, control channel):
### Settings that change behaviour
- `general.language`: interface language (`system` / `en` / `ja` / `zh-hans` / `zh-hant` / `uk`), Settings ▸ General ▸ Language; menu labels change immediately. `system` (Match System, the default) follows the operating system's interface language where EffectCraft has a translation and is English otherwise: a Simplified Chinese locale (`zh`, `zh-CN`, `zh_CN.UTF-8`, `zh-Hans-CN`, `zh-SG`) selects `zh-hans`, a Traditional one (`zh-TW`, `zh-HK`, `zh-Hant`, `zh_MO`) selects `zh-hant`, a Ukrainian one (`uk`, `uk-UA`, `uk_UA.UTF-8`) selects `uk`; the browser build stays in English with it. Choose **Українська** for Ukrainian menus. Dialog and panel contents retain their current English labels, as with the other menu translations. Ukrainian uses the bundled UI fonts. Native Japanese, Simplified and Traditional Chinese UI uses installed system fonts; the web host must supply a Japanese font.
- `general.language`: interface language (`system` / `en` / `ja` / `zh-hans` / `zh-hant` / `uk`), Settings ▸ General ▸ Language; menu labels change immediately. `system` (Match System, the default) follows the operating system's interface language where EffectCraft has a translation and is English otherwise: a Simplified Chinese locale (`zh`, `zh-CN`, `zh_CN.UTF-8`, `zh-Hans-CN`, `zh-SG`) selects `zh-hans`, a Traditional one (`zh-TW`, `zh-HK`, `zh-Hant`, `zh_MO`) selects `zh-hant`, a Ukrainian one (`uk`, `uk-UA`, `uk_UA.UTF-8`) selects `uk`; the browser build stays in English with it. Choose **Українська** for Ukrainian menus. Ukrainian dialog and panel contents stay in English for now. Ukrainian uses the bundled UI fonts. Native Japanese, Simplified and Traditional Chinese UI uses installed system fonts; the web host must supply a Japanese font. Two catalogs back the text: the menus are keyed by `(command, English label)` (`crates/ui-egui/src/i18n.rs`), and the rest of the interface - panels, dialogs, buttons, tooltips - is keyed by its English source string (`crates/ui-egui/src/i18n/ui.rs`), where a panel writes `tr("Rename")`, or `tr_args("Proxy Use: {}", &[&quality])` when the row carries a value. A row that is missing falls back to English, so a new string shows up untranslated rather than empty; the tests in that module fail on an unused row and on a hard-coded string in a converted panel.
- `general.undoLevels`: Levels of Undo
- `general.pathPointSize`: Path Point and Handle Size
- `general.recentItems`: Recent Projects Shown