feat(ui): Catppuccin themes, View > Preferences > Theme and a custom title bar
Adds the four Catppuccin themes, the View > Preferences > Theme menu, and a window-drawn title bar on Windows and Linux (Settings > Appearance > Use System Title Bar opts out). macOS gets the traffic lights over the top row. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,3 +22,7 @@ sub the Tools bar and About dialog carry the ArtCraft mark => the Tools bar and
|
||||
forbid (?i)artcraft(?! Team and the EffectCraft contributors)
|
||||
forbid (?i)discord
|
||||
sub ffmpeg export, Discord presence, => ffmpeg export,
|
||||
dropline ^crates/ui-egui/src/i18n(?:\.rs|/ui\.rs)$ :: \s*\(".*(?:ArtCraft|Discord)
|
||||
sub (?m)^ \(\n "[^"\n]*ArtCraft[^"\n]*",\n "[^"\n]*",\n \),\n =>
|
||||
sub \(#612, reported on Discord\) => (#612, reported by a user)
|
||||
sub Reported on Discord: => Reported by a user:
|
||||
|
||||
@@ -45,6 +45,21 @@ fn app_icon() -> egui::IconData {
|
||||
|
||||
/// Both native JSON projects and Save a Copy As XML projects are openable on launch.
|
||||
/// Keep this classification shared by project opening and the subsequent media-import pass.
|
||||
/// Windows and Linux: no OS title bar; the menu bar row is the title bar
|
||||
/// (`effectcraft_ui_egui::titlebar`). macOS keeps its traffic lights over the integrated top row.
|
||||
const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos");
|
||||
|
||||
/// Whether this start draws its own title bar: Windows and Linux do, unless Settings ▸ Appearance ▸
|
||||
/// Use System Title Bar asks for the system's. Read from the saved settings before the window
|
||||
/// opens; a missing or unreadable file keeps the default.
|
||||
fn custom_titlebar(config_dir: Option<&std::path::Path>) -> bool {
|
||||
let prefs = config_dir
|
||||
.and_then(|d| std::fs::read_to_string(d.join(effectcraft_engine::prefs::PREFS_FILE)).ok())
|
||||
.map(|t| effectcraft_engine::prefs::Prefs::from_json(&t))
|
||||
.unwrap_or_default();
|
||||
CUSTOM_TITLEBAR && !prefs.appearance.system_title_bar
|
||||
}
|
||||
|
||||
fn is_project_file(path: &str) -> bool {
|
||||
std::path::Path::new(path)
|
||||
.extension()
|
||||
@@ -117,12 +132,17 @@ fn main() -> eframe::Result {
|
||||
let mut launch = launch_guard::Launch::begin(effectcraft_host::config_dir().as_deref(), std::env::var_os("WGPU_BACKEND").is_some());
|
||||
let on_gl = launch.backends == Some(eframe::wgpu::Backends::GL);
|
||||
let notice = launch.notice.take();
|
||||
let custom_titlebar = custom_titlebar(effectcraft_host::config_dir().as_deref());
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_title("EffectCraft")
|
||||
.with_inner_size(window::DEFAULT_SIZE)
|
||||
.with_min_inner_size(window::MIN_SIZE)
|
||||
.with_drag_and_drop(true)
|
||||
.with_decorations(!custom_titlebar)
|
||||
.with_fullsize_content_view(true)
|
||||
.with_titlebar_shown(false)
|
||||
.with_title_shown(false)
|
||||
// Started for an agent (`--control`): open without taking the user's keyboard focus.
|
||||
.with_active(control_port.is_none())
|
||||
.with_icon(app_icon())
|
||||
@@ -205,6 +225,8 @@ fn main() -> eframe::Result {
|
||||
// reported in a toast and forgotten).
|
||||
let _ = session.execute("window.restoreScriptPanels", json!({}));
|
||||
let mut app = EffectcraftApp::new(session);
|
||||
app.custom_titlebar = custom_titlebar;
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
app.set_gpu_failure_bridge(gpu_failures);
|
||||
app.ui.start_screen = show_home;
|
||||
if let Some(n) = notice {
|
||||
@@ -424,7 +446,20 @@ fn disable_app_nap() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Args, parse_args};
|
||||
use super::{Args, custom_titlebar, parse_args};
|
||||
|
||||
#[test]
|
||||
fn the_system_title_bar_setting_keeps_the_window_decorations() {
|
||||
let dir = std::env::temp_dir().join(format!("ec-titlebar-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let file = dir.join(effectcraft_engine::prefs::PREFS_FILE);
|
||||
assert_eq!(custom_titlebar(None), super::CUSTOM_TITLEBAR, "no settings folder: the default");
|
||||
std::fs::write(&file, "not json").unwrap();
|
||||
assert_eq!(custom_titlebar(Some(&dir)), super::CUSTOM_TITLEBAR, "an unreadable file: the default");
|
||||
std::fs::write(&file, r#"{"appearance":{"systemTitleBar":true}}"#).unwrap();
|
||||
assert!(!custom_titlebar(Some(&dir)));
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
fn parse(args: &[&str]) -> Option<Args> {
|
||||
parse_args(args.iter().map(|a| a.to_string()), None)
|
||||
|
||||
@@ -37,6 +37,16 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
fe!("app.keyboardShortcuts", "Keyboard Shortcuts", ["Edit"], Some("Cmd+Alt+'"), "{}", always),
|
||||
fe!("app.templates", "Templates", [], None, "{kind: renderSettings|outputModule}", always),
|
||||
fe!("app.find", "Find", ["File"], Some("Cmd+F"), "{query?}", always),
|
||||
// Themes (View ▸ Preferences ▸ Theme): `view.theme.toggle` cycles the appearance mode, the
|
||||
// others show that theme and fix the mode to its family (see `appearance.theme`).
|
||||
fe!("view.theme.toggle", "Next Appearance Mode", [], None, "{}", always),
|
||||
fe!("view.theme.dark", "Theme: Dark", [], None, "{}", always),
|
||||
fe!("view.theme.darker", "Theme: Darker", [], None, "{}", always),
|
||||
fe!("view.theme.light", "Theme: Light", [], None, "{}", always),
|
||||
fe!("view.theme.catppuccinLatte", "Theme: Catppuccin Latte", [], None, "{}", always),
|
||||
fe!("view.theme.catppuccinFrappe", "Theme: Catppuccin Frappé", [], None, "{}", always),
|
||||
fe!("view.theme.catppuccinMacchiato", "Theme: Catppuccin Macchiato", [], None, "{}", always),
|
||||
fe!("view.theme.catppuccinMocha", "Theme: Catppuccin Mocha", [], None, "{}", always),
|
||||
// Preview.
|
||||
fe!(
|
||||
"playback.toggle",
|
||||
|
||||
@@ -1150,6 +1150,20 @@ View
|
||||
---
|
||||
Go to Time... | time.set
|
||||
Enter Full Screen | view.fullScreen
|
||||
---
|
||||
Preferences
|
||||
Theme
|
||||
Dark | view.theme.dark
|
||||
Darker | view.theme.darker
|
||||
Light | view.theme.light
|
||||
---
|
||||
Catppuccin Latte | view.theme.catppuccinLatte
|
||||
Catppuccin Frappé | view.theme.catppuccinFrappe
|
||||
Catppuccin Macchiato | view.theme.catppuccinMacchiato
|
||||
Catppuccin Mocha | view.theme.catppuccinMocha
|
||||
---
|
||||
Next Appearance Mode | view.theme.toggle
|
||||
Appearance... | app.settings {"page":"appearance"}
|
||||
Window
|
||||
Workspace
|
||||
Default | window.workspace {"name":"Default"} | Shift+F10
|
||||
|
||||
@@ -46,13 +46,27 @@ pub const WINDOW_GRAPHICS: &[(&str, &str)] = &[("Automatic", "auto"), ("OpenGL (
|
||||
pub const APPEARANCE_MODES: &[(&str, &str)] = &[("Sync with System", "auto"), ("Dark", "dark"), ("Light", "light")];
|
||||
|
||||
/// The dark family of themes: (label, `appearance.darkTheme` value).
|
||||
pub const DARK_THEMES: &[(&str, &str)] = &[("Dark", "dark"), ("Darker", "darker")];
|
||||
pub const DARK_THEMES: &[(&str, &str)] = &[
|
||||
("Dark", "dark"),
|
||||
("Darker", "darker"),
|
||||
("Catppuccin Frappé", "catppuccinFrappe"),
|
||||
("Catppuccin Macchiato", "catppuccinMacchiato"),
|
||||
("Catppuccin Mocha", "catppuccinMocha"),
|
||||
];
|
||||
|
||||
/// The light family of themes: (label, `appearance.lightTheme` value).
|
||||
pub const LIGHT_THEMES: &[(&str, &str)] = &[("Light", "light")];
|
||||
pub const LIGHT_THEMES: &[(&str, &str)] = &[("Light", "light"), ("Catppuccin Latte", "catppuccinLatte")];
|
||||
|
||||
/// Every theme (the legacy single-theme `appearance.theme`).
|
||||
pub const THEMES: &[(&str, &str)] = &[("Dark", "dark"), ("Darker", "darker"), ("Light", "light")];
|
||||
pub const THEMES: &[(&str, &str)] = &[
|
||||
("Dark", "dark"),
|
||||
("Darker", "darker"),
|
||||
("Light", "light"),
|
||||
("Catppuccin Latte", "catppuccinLatte"),
|
||||
("Catppuccin Frappé", "catppuccinFrappe"),
|
||||
("Catppuccin Macchiato", "catppuccinMacchiato"),
|
||||
("Catppuccin Mocha", "catppuccinMocha"),
|
||||
];
|
||||
|
||||
/// The settings whose value must be one of their choices.
|
||||
fn choices(key: &str) -> Option<&'static [(&'static str, &'static str)]> {
|
||||
@@ -188,6 +202,9 @@ page!(Appearance {
|
||||
use_gradients: bool = true,
|
||||
/// macOS: draw the menu bar inside the window instead of the system menu bar.
|
||||
in_window_menu_bar_mac: bool = false,
|
||||
/// Windows and Linux: keep the system's title bar instead of drawing the window's own.
|
||||
/// Read before the window opens: it applies from the next launch.
|
||||
system_title_bar: bool = false,
|
||||
});
|
||||
|
||||
page!(Grids {
|
||||
@@ -1242,6 +1259,8 @@ pub fn pages() -> Vec<Page> {
|
||||
s("appearance.useGradients", "Use Gradients", B, true),
|
||||
Section("Menu Bar"),
|
||||
s("appearance.inWindowMenuBarMac", "Use In-Window Menu Bar on macOS", B, true),
|
||||
Section("Title Bar"),
|
||||
s("appearance.systemTitleBar", "Use System Title Bar on Windows and Linux", B, true),
|
||||
],
|
||||
},
|
||||
Page {
|
||||
|
||||
@@ -413,6 +413,15 @@ const SIMPLIFIED_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("app.templates", "Output Module...", "输出模块..."),
|
||||
("app.keyboardShortcuts", "Keyboard Shortcuts", "键盘快捷键"),
|
||||
("", "Preferences", "首选项"),
|
||||
("", "Theme", "主题"),
|
||||
("view.theme.dark", "Dark", "深色"),
|
||||
("view.theme.darker", "Darker", "更深"),
|
||||
("view.theme.light", "Light", "浅色"),
|
||||
("view.theme.catppuccinLatte", "Catppuccin Latte", "Catppuccin Latte"),
|
||||
("view.theme.catppuccinFrappe", "Catppuccin Frappé", "Catppuccin Frappé"),
|
||||
("view.theme.catppuccinMacchiato", "Catppuccin Macchiato", "Catppuccin Macchiato"),
|
||||
("view.theme.catppuccinMocha", "Catppuccin Mocha", "Catppuccin Mocha"),
|
||||
("view.theme.toggle", "Next Appearance Mode", "下一个外观模式"),
|
||||
("", "Composition", "合成"),
|
||||
("comp.new", "New Composition...", "新建合成..."),
|
||||
("comp.settings", "Composition Settings...", "合成设置..."),
|
||||
@@ -1040,6 +1049,15 @@ const TRADITIONAL_CHINESE: &[(&str, &str, &str)] = &[
|
||||
("app.templates", "Output Module...", "輸出模組..."),
|
||||
("app.keyboardShortcuts", "Keyboard Shortcuts", "鍵盤快速鍵"),
|
||||
("", "Preferences", "偏好設定"),
|
||||
("", "Theme", "主題"),
|
||||
("view.theme.dark", "Dark", "深色"),
|
||||
("view.theme.darker", "Darker", "更深"),
|
||||
("view.theme.light", "Light", "淺色"),
|
||||
("view.theme.catppuccinLatte", "Catppuccin Latte", "Catppuccin Latte"),
|
||||
("view.theme.catppuccinFrappe", "Catppuccin Frappé", "Catppuccin Frappé"),
|
||||
("view.theme.catppuccinMacchiato", "Catppuccin Macchiato", "Catppuccin Macchiato"),
|
||||
("view.theme.catppuccinMocha", "Catppuccin Mocha", "Catppuccin Mocha"),
|
||||
("view.theme.toggle", "Next Appearance Mode", "下一個外觀模式"),
|
||||
("", "Composition", "合成"),
|
||||
("comp.new", "New Composition...", "新增合成..."),
|
||||
("comp.settings", "Composition Settings...", "合成設定..."),
|
||||
@@ -1664,6 +1682,15 @@ const JAPANESE: &[(&str, &str, &str)] = &[
|
||||
("app.templates", "Output Module...", "出力モジュール..."),
|
||||
("app.keyboardShortcuts", "Keyboard Shortcuts", "キーボードショートカット"),
|
||||
("", "Preferences", "環境設定"),
|
||||
("", "Theme", "テーマ"),
|
||||
("view.theme.dark", "Dark", "ダーク"),
|
||||
("view.theme.darker", "Darker", "暗く"),
|
||||
("view.theme.light", "Light", "ライト"),
|
||||
("view.theme.catppuccinLatte", "Catppuccin Latte", "Catppuccin Latte"),
|
||||
("view.theme.catppuccinFrappe", "Catppuccin Frappé", "Catppuccin Frappé"),
|
||||
("view.theme.catppuccinMacchiato", "Catppuccin Macchiato", "Catppuccin Macchiato"),
|
||||
("view.theme.catppuccinMocha", "Catppuccin Mocha", "Catppuccin Mocha"),
|
||||
("view.theme.toggle", "Next Appearance Mode", "次の外観モード"),
|
||||
("", "Composition", "コンポジション"),
|
||||
("comp.new", "New Composition...", "新規コンポジション..."),
|
||||
("comp.settings", "Composition Settings...", "コンポジション設定..."),
|
||||
@@ -2291,6 +2318,15 @@ const UKRAINIAN: &[(&str, &str, &str)] = &[
|
||||
("app.templates", "Output Module...", "Модуль виведення..."),
|
||||
("app.keyboardShortcuts", "Keyboard Shortcuts", "Клавіатурні скорочення"),
|
||||
("", "Preferences", "Налаштування"),
|
||||
("", "Theme", "Тема"),
|
||||
("view.theme.dark", "Dark", "Темна"),
|
||||
("view.theme.darker", "Darker", "Темніше"),
|
||||
("view.theme.light", "Light", "Світла"),
|
||||
("view.theme.catppuccinLatte", "Catppuccin Latte", "Catppuccin Latte"),
|
||||
("view.theme.catppuccinFrappe", "Catppuccin Frappé", "Catppuccin Frappé"),
|
||||
("view.theme.catppuccinMacchiato", "Catppuccin Macchiato", "Catppuccin Macchiato"),
|
||||
("view.theme.catppuccinMocha", "Catppuccin Mocha", "Catppuccin Mocha"),
|
||||
("view.theme.toggle", "Next Appearance Mode", "Наступний режим вигляду"),
|
||||
("", "Composition", "Композиція"),
|
||||
("comp.new", "New Composition...", "Нова композиція..."),
|
||||
("comp.settings", "Composition Settings...", "Налаштування композиції..."),
|
||||
@@ -2919,6 +2955,15 @@ const SPANISH: &[(&str, &str, &str)] = &[
|
||||
("app.templates", "Output Module...", "Módulo de salida..."),
|
||||
("app.keyboardShortcuts", "Keyboard Shortcuts", "Métodos abreviados de teclado"),
|
||||
("", "Preferences", "Preferencias"),
|
||||
("", "Theme", "Tema"),
|
||||
("view.theme.dark", "Dark", "Oscuro"),
|
||||
("view.theme.darker", "Darker", "Más oscuro"),
|
||||
("view.theme.light", "Light", "Claro"),
|
||||
("view.theme.catppuccinLatte", "Catppuccin Latte", "Catppuccin Latte"),
|
||||
("view.theme.catppuccinFrappe", "Catppuccin Frappé", "Catppuccin Frappé"),
|
||||
("view.theme.catppuccinMacchiato", "Catppuccin Macchiato", "Catppuccin Macchiato"),
|
||||
("view.theme.catppuccinMocha", "Catppuccin Mocha", "Catppuccin Mocha"),
|
||||
("view.theme.toggle", "Next Appearance Mode", "Siguiente modo de apariencia"),
|
||||
("", "Composition", "Composición"),
|
||||
("comp.new", "New Composition...", "Nueva composición..."),
|
||||
("comp.settings", "Composition Settings...", "Configuración de la composición..."),
|
||||
@@ -3545,6 +3590,15 @@ const PORTUGUESE: &[(&str, &str, &str)] = &[
|
||||
("app.templates", "Output Module...", "Módulo de saída..."),
|
||||
("app.keyboardShortcuts", "Keyboard Shortcuts", "Atalhos de teclado"),
|
||||
("", "Preferences", "Preferências"),
|
||||
("", "Theme", "Tema"),
|
||||
("view.theme.dark", "Dark", "Escuro"),
|
||||
("view.theme.darker", "Darker", "Mais escuro"),
|
||||
("view.theme.light", "Light", "Claro"),
|
||||
("view.theme.catppuccinLatte", "Catppuccin Latte", "Catppuccin Latte"),
|
||||
("view.theme.catppuccinFrappe", "Catppuccin Frappé", "Catppuccin Frappé"),
|
||||
("view.theme.catppuccinMacchiato", "Catppuccin Macchiato", "Catppuccin Macchiato"),
|
||||
("view.theme.catppuccinMocha", "Catppuccin Mocha", "Catppuccin Mocha"),
|
||||
("view.theme.toggle", "Next Appearance Mode", "Próximo modo de aparência"),
|
||||
("", "Composition", "Composição"),
|
||||
("comp.new", "New Composition...", "Nova composição..."),
|
||||
("comp.settings", "Composition Settings...", "Configurações da composição..."),
|
||||
@@ -4635,9 +4689,7 @@ const RUSSIAN: &[(&str, &str, &str)] = &[
|
||||
("help.systemReport", "System Compatibility Report...", "Отчет о совместимости системы..."),
|
||||
("help.enableLogging", "Enable Logging", "Включить ведение журнала"),
|
||||
("help.revealLogFile", "Reveal Logging File", "Показать файл журнала"),
|
||||
("help.discord", "Join the ArtCraft Discord...", "Присоединяйтесь к Discord ArtCraft..."),
|
||||
("help.reportIssue", "Provide Feedback...", "Оставить отзыв..."),
|
||||
("help.website", "ArtCraft Website", "Сайт ArtCraft"),
|
||||
("help.appPage", "EffectCraft Home Page", "Главная страница EffectCraft"),
|
||||
("help.github", "EffectCraft on GitHub", "EffectCraft на GitHub"),
|
||||
("file.openDemoProject", "Open Demo Project", "Открыть демонстрационный проект"),
|
||||
@@ -5264,9 +5316,7 @@ const FRENCH: &[(&str, &str, &str)] = &[
|
||||
("help.systemReport", "System Compatibility Report...", "Rapport de compatibilité du système..."),
|
||||
("help.enableLogging", "Enable Logging", "Activer la journalisation"),
|
||||
("help.revealLogFile", "Reveal Logging File", "Révéler le fichier journal"),
|
||||
("help.discord", "Join the ArtCraft Discord...", "Rejoindre le Discord ArtCraft..."),
|
||||
("help.reportIssue", "Provide Feedback...", "Envoyer des commentaires..."),
|
||||
("help.website", "ArtCraft Website", "Site web ArtCraft"),
|
||||
("help.appPage", "EffectCraft Home Page", "Page d’accueil EffectCraft"),
|
||||
("help.github", "EffectCraft on GitHub", "EffectCraft sur GitHub"),
|
||||
("file.openDemoProject", "Open Demo Project", "Ouvrir le projet de démonstration"),
|
||||
@@ -5893,9 +5943,7 @@ const GERMAN: &[(&str, &str, &str)] = &[
|
||||
("help.systemReport", "System Compatibility Report...", "Bericht zur Systemkompatibilität..."),
|
||||
("help.enableLogging", "Enable Logging", "Protokollierung aktivieren"),
|
||||
("help.revealLogFile", "Reveal Logging File", "Protokolldatei anzeigen"),
|
||||
("help.discord", "Join the ArtCraft Discord...", "ArtCraft-Discord beitreten..."),
|
||||
("help.reportIssue", "Provide Feedback...", "Feedback senden..."),
|
||||
("help.website", "ArtCraft Website", "ArtCraft-Website"),
|
||||
("help.appPage", "EffectCraft Home Page", "EffectCraft-Startseite"),
|
||||
("help.github", "EffectCraft on GitHub", "EffectCraft auf GitHub"),
|
||||
("file.openDemoProject", "Open Demo Project", "Demoprojekt öffnen"),
|
||||
@@ -6523,9 +6571,7 @@ const CZECH: &[(&str, &str, &str)] = &[
|
||||
("help.systemReport", "System Compatibility Report...", "Zpráva o kompatibilitě systému..."),
|
||||
("help.enableLogging", "Enable Logging", "Povolit protokolování"),
|
||||
("help.revealLogFile", "Reveal Logging File", "Zobrazit soubor protokolu"),
|
||||
("help.discord", "Join the ArtCraft Discord...", "Připojit se na Discord ArtCraft..."),
|
||||
("help.reportIssue", "Provide Feedback...", "Poskytnout zpětnou vazbu..."),
|
||||
("help.website", "ArtCraft Website", "Web ArtCraft"),
|
||||
("help.appPage", "EffectCraft Home Page", "Domovská stránka EffectCraft"),
|
||||
("help.github", "EffectCraft on GitHub", "EffectCraft na GitHubu"),
|
||||
("file.openDemoProject", "Open Demo Project", "Otevřít ukázkový projekt"),
|
||||
|
||||
@@ -522,6 +522,11 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("⌘/Ctrl-drag a pin to record its motion", "⌘/Ctrl+ドラッグでピンの動きを記録"),
|
||||
("▣ {}", "▣ {}"),
|
||||
("⚠ {} is used by:", "⚠ {}は次のコマンドで使用中:"),
|
||||
("Minimize", "最小化"),
|
||||
("Maximize", "最大化"),
|
||||
("Restore", "元に戻す"),
|
||||
("Title Bar", "タイトルバー"),
|
||||
("Use System Title Bar on Windows and Linux", "Windows と Linux でシステムのタイトルバーを使用"),
|
||||
];
|
||||
|
||||
const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
@@ -1052,6 +1057,11 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
("After Effects project", "After Effects 项目"),
|
||||
("Lottie animation", "Lottie 动画"),
|
||||
("Project", "项目"),
|
||||
("Minimize", "最小化"),
|
||||
("Maximize", "最大化"),
|
||||
("Restore", "还原"),
|
||||
("Title Bar", "标题栏"),
|
||||
("Use System Title Bar on Windows and Linux", "在 Windows 和 Linux 上使用系统标题栏"),
|
||||
];
|
||||
|
||||
const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
@@ -1577,6 +1587,11 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
("After Effects project", "After Effects 專案"),
|
||||
("Lottie animation", "Lottie 動畫"),
|
||||
("Project", "專案"),
|
||||
("Minimize", "最小化"),
|
||||
("Maximize", "最大化"),
|
||||
("Restore", "還原"),
|
||||
("Title Bar", "標題列"),
|
||||
("Use System Title Bar on Windows and Linux", "在 Windows 和 Linux 上使用系統標題列"),
|
||||
];
|
||||
|
||||
/// Ukrainian (uk): original translations of the same interface rows as the Chinese catalogs.
|
||||
@@ -2084,7 +2099,6 @@ const UKRAINIAN: &[(&str, &str)] = &[
|
||||
("Import PDF / Illustrator File", "Імпорт файлу PDF / Illustrator"),
|
||||
("Import Photoshop File", "Імпорт файлу Photoshop"),
|
||||
("Interpret Unlabeled Alpha As", "Інтерпретувати непозначену альфу як"),
|
||||
("Join the ArtCraft Discord", "Приєднатися до Discord ArtCraft"),
|
||||
("Mercury GPU Acceleration ({})", "Прискорення Mercury GPU ({})"),
|
||||
("Mercury Software Only", "Лише програмний Mercury"),
|
||||
("OCIO (built-in ACES config)", "OCIO (вбудована конфігурація ACES)"),
|
||||
@@ -2130,6 +2144,11 @@ const UKRAINIAN: &[(&str, &str)] = &[
|
||||
("After Effects project", "Проєкт After Effects"),
|
||||
("Lottie animation", "Анімація Lottie"),
|
||||
("Project", "Проєкт"),
|
||||
("Minimize", "Згорнути"),
|
||||
("Maximize", "Розгорнути"),
|
||||
("Restore", "Відновити"),
|
||||
("Title Bar", "Рядок заголовка"),
|
||||
("Use System Title Bar on Windows and Linux", "Використовувати системний рядок заголовка у Windows і Linux"),
|
||||
];
|
||||
|
||||
// Brazilian Portuguese.
|
||||
@@ -2140,10 +2159,6 @@ const PORTUGUESE: &[(&str, &str)] = &[
|
||||
("(missing)", "(ausente)"),
|
||||
("360 View Orientation", "Orientação da vista 360°"),
|
||||
("3:2 Pulldown: {}", "Pulldown 3:2: {}"),
|
||||
(
|
||||
"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.",
|
||||
"Um compositor de código aberto, desenvolvido em clean-room, para motion graphics e efeitos visuais: nativo no macOS, Windows e Linux, e no navegador. Parte da família de aplicativos criativos ArtCraft.",
|
||||
),
|
||||
("About EffectCraft", "Sobre o EffectCraft"),
|
||||
("About...", "Sobre..."),
|
||||
("Active Camera", "Câmera ativa"),
|
||||
@@ -2237,7 +2252,6 @@ const PORTUGUESE: &[(&str, &str)] = &[
|
||||
("Delete selected project items", "Excluir itens de projeto selecionados"),
|
||||
("Delete this template", "Excluir este modelo"),
|
||||
("Depth of Field", "Profundidade de campo"),
|
||||
("Discord", "Discord"),
|
||||
("Distribute Layers:", "Distribuir camadas:"),
|
||||
("Don't Recover", "Não recuperar"),
|
||||
("Don't Save", "Não salvar"),
|
||||
@@ -2308,7 +2322,6 @@ const PORTUGUESE: &[(&str, &str)] = &[
|
||||
("Install from File…", "Instalar a partir de arquivo…"),
|
||||
("Intensity", "Intensidade"),
|
||||
("Inverted", "Invertido"),
|
||||
("Join the ArtCraft community on Discord", "Entre na comunidade ArtCraft no Discord"),
|
||||
("Key: {}", "Tecla: {}"),
|
||||
("LEARN", "APRENDER"),
|
||||
("Label", "Rótulo"),
|
||||
@@ -2344,7 +2357,6 @@ const PORTUGUESE: &[(&str, &str)] = &[
|
||||
("Models folder: {}", "Pasta de modelos: {}"),
|
||||
("Modifiers:", "Modificadores:"),
|
||||
("More", "Mais"),
|
||||
("More ArtCraft apps", "Mais aplicativos ArtCraft"),
|
||||
("More tutorials", "Mais tutoriais"),
|
||||
("Motion Blur", "Desfoque de movimento"),
|
||||
("Motion Blur: {}", "Desfoque de movimento: {}"),
|
||||
@@ -2574,10 +2586,6 @@ const CZECH: &[(&str, &str)] = &[
|
||||
("(missing)", "(chybí)"),
|
||||
("360 View Orientation", "Orientace zobrazení 360°"),
|
||||
("3:2 Pulldown: {}", "Převod 3:2 pulldown: {}"),
|
||||
(
|
||||
"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.",
|
||||
"Kompoziční nástroj s otevřeným zdrojovým kódem pro pohyblivou grafiku a vizuální efekty, vyvinutý nezávisle metodou clean-room. Běží nativně na macOS, Windows a Linuxu i v prohlížeči. Patří do rodiny kreativních aplikací ArtCraft.",
|
||||
),
|
||||
("About EffectCraft", "O aplikaci EffectCraft"),
|
||||
("About...", "O aplikaci…"),
|
||||
("Active", "Aktivní"),
|
||||
@@ -2678,7 +2686,6 @@ const CZECH: &[(&str, &str)] = &[
|
||||
("Delete selected project items", "Odstranit vybrané položky projektu"),
|
||||
("Delete this template", "Odstranit tuto šablonu"),
|
||||
("Depth of Field", "Hloubka ostrosti"),
|
||||
("Discord", "Discord"),
|
||||
("Distribute Layers:", "Rozmístit vrstvy:"),
|
||||
("Don't Recover", "Neobnovovat"),
|
||||
("Don't Save", "Neukládat"),
|
||||
@@ -2750,7 +2757,6 @@ const CZECH: &[(&str, &str)] = &[
|
||||
("Install from File…", "Nainstalovat ze souboru…"),
|
||||
("Intensity", "Intenzita"),
|
||||
("Inverted", "Invertovaná"),
|
||||
("Join the ArtCraft community on Discord", "Připojit se ke komunitě ArtCraft na Discordu"),
|
||||
("Key: {}", "Klávesa: {}"),
|
||||
("LEARN", "VÝUKA"),
|
||||
("Label", "Štítek"),
|
||||
@@ -2788,7 +2794,6 @@ const CZECH: &[(&str, &str)] = &[
|
||||
("Models folder: {}", "Složka modelů: {}"),
|
||||
("Modifiers:", "Modifikátory:"),
|
||||
("More", "Další"),
|
||||
("More ArtCraft apps", "Další aplikace ArtCraft"),
|
||||
("More tutorials", "Další výukové lekce"),
|
||||
("Motion Blur", "Rozostření pohybem"),
|
||||
("Motion Blur: {}", "Rozostření pohybem: {}"),
|
||||
@@ -3048,7 +3053,6 @@ const CZECH: &[(&str, &str)] = &[
|
||||
("Import PDF / Illustrator File", "Importovat soubor PDF / Illustrator"),
|
||||
("Import Photoshop File", "Importovat soubor Photoshop"),
|
||||
("Interpret Unlabeled Alpha As", "Interpretovat neoznačenou alfu jako"),
|
||||
("Join the ArtCraft Discord", "Připojit se na Discord ArtCraft"),
|
||||
("Mercury GPU Acceleration ({})", "GPU akcelerace Mercury ({})"),
|
||||
("Mercury Software Only", "Pouze softwarové zpracování Mercury"),
|
||||
("OCIO (built-in ACES config)", "OCIO (vestavěná konfigurace ACES)"),
|
||||
@@ -3111,6 +3115,7 @@ mod tests {
|
||||
("color_picker.rs", include_str!("../color_picker.rs")),
|
||||
("credits.rs", include_str!("../credits.rs")),
|
||||
("header.rs", include_str!("../header.rs")),
|
||||
("titlebar.rs", include_str!("../titlebar.rs")),
|
||||
("menus.rs", include_str!("../menus.rs")),
|
||||
("widgets.rs", include_str!("../widgets.rs")),
|
||||
("panels/anim_tools.rs", include_str!("../panels/anim_tools.rs")),
|
||||
|
||||
@@ -30,6 +30,7 @@ mod pointer_wrap;
|
||||
pub mod prefs_live;
|
||||
pub mod state;
|
||||
pub mod theme;
|
||||
pub mod titlebar;
|
||||
pub mod widgets;
|
||||
mod workspace_state;
|
||||
|
||||
@@ -318,7 +319,10 @@ pub struct EffectcraftApp {
|
||||
/// Pointer position in comp pixels (Info panel).
|
||||
pub(crate) pointer_comp: Option<[f32; 2]>,
|
||||
pub(crate) dialog_state: panels::dialogs::DialogState,
|
||||
/// macOS: the traffic lights sit over the top row (no system title bar).
|
||||
pub integrated_titlebar: bool,
|
||||
/// Windows and Linux: no OS decorations; the menu bar row is the title bar ([`titlebar`]).
|
||||
pub custom_titlebar: bool,
|
||||
/// Audio preview while playing (audio clock drives playback).
|
||||
pub audio: Option<audio::AudioPlayback>,
|
||||
/// Audio scrubbing's open output (Ctrl/Cmd-drag the current time, `playback.scrubAudio`).
|
||||
@@ -409,6 +413,7 @@ impl EffectcraftApp {
|
||||
pointer_comp: None,
|
||||
dialog_state: Default::default(),
|
||||
integrated_titlebar: false,
|
||||
custom_titlebar: false,
|
||||
audio: None,
|
||||
scrub: None,
|
||||
meter: Default::default(),
|
||||
@@ -1661,15 +1666,28 @@ impl EffectcraftApp {
|
||||
}
|
||||
ui.painter().rect_filled(full, 0.0, t.app_bg);
|
||||
let mut top = full.min.y;
|
||||
if self.ui.show_menu_bar {
|
||||
let mb = egui::Rect::from_min_size(full.min, egui::vec2(full.width(), 24.0));
|
||||
if self.ui.show_menu_bar || self.custom_titlebar {
|
||||
let h = if self.custom_titlebar { titlebar::HEIGHT } else { 24.0 };
|
||||
let mb = egui::Rect::from_min_size(full.min, egui::vec2(full.width(), h));
|
||||
if self.custom_titlebar || self.integrated_titlebar {
|
||||
titlebar::drag_area(ui, mb);
|
||||
}
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(mb));
|
||||
child.painter().rect_filled(mb, 0.0, t.header_bg);
|
||||
menus::menu_bar(self, &mut child);
|
||||
if self.ui.show_menu_bar {
|
||||
menus::menu_bar(self, &mut child);
|
||||
}
|
||||
if self.custom_titlebar {
|
||||
titlebar::caption_buttons(self, ui, mb);
|
||||
}
|
||||
top = mb.max.y;
|
||||
}
|
||||
let header_h = 40.0;
|
||||
let header = egui::Rect::from_min_size(egui::pos2(full.min.x, top), egui::vec2(full.width(), header_h));
|
||||
// macOS without a menu row: the Tools bar is the title bar, under the traffic lights.
|
||||
if self.integrated_titlebar && !self.ui.show_menu_bar {
|
||||
titlebar::drag_area(ui, header);
|
||||
}
|
||||
header::show(self, ui, header);
|
||||
let body = egui::Rect::from_min_max(egui::pos2(full.min.x + 4.0, header.max.y + 2.0), egui::pos2(full.max.x - 4.0, full.max.y - 4.0));
|
||||
self.dock_area(ui, body);
|
||||
@@ -1686,6 +1704,9 @@ impl EffectcraftApp {
|
||||
panels::learn::coach(self, &ctx);
|
||||
let lift = panels::media_panels::import_card(self, ui, full);
|
||||
self.draw_toast(ui, full, lift);
|
||||
if self.custom_titlebar {
|
||||
titlebar::resize_zones(ui);
|
||||
}
|
||||
if self.session.config.is_some() && self.workspace_store.has_changes(&self.ui) && !ctx.input(|i| i.pointer.any_down()) {
|
||||
if self.workspace_store.due(ctx.input(|i| i.time)) {
|
||||
self.save_workspace_state();
|
||||
|
||||
@@ -46,10 +46,6 @@ pub const UI_COMMANDS: &[UiCommand] = &[
|
||||
uic!("view.safeMargins", "Title/Action Safe", [], None),
|
||||
uic!("view.transparencyGrid", "Transparency Grid", [], None),
|
||||
uic!("view.fastPreviews", "Fast Previews", [], None),
|
||||
uic!("view.theme.toggle", "Next Appearance Mode", [], None),
|
||||
uic!("view.theme.dark", "Theme: Dark", [], None),
|
||||
uic!("view.theme.darker", "Theme: Darker", [], None),
|
||||
uic!("view.theme.light", "Theme: Light", [], None),
|
||||
uic!("timeline.zoomIn", "Zoom In Time", [], Some("=")),
|
||||
uic!("timeline.zoomOut", "Zoom Out Time", [], Some("-")),
|
||||
uic!("timeline.zoomFit", "Zoom to Fit Comp", [], None),
|
||||
@@ -1833,7 +1829,8 @@ pub fn menu_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
|
||||
let mut clicked: Option<(String, Value)> = None;
|
||||
let mut nav = crate::menu_keys::Nav::default();
|
||||
ui.horizontal_centered(|ui| {
|
||||
ui.add_space(6.0);
|
||||
// macOS: clear of the traffic lights over the row.
|
||||
ui.add_space(if app.integrated_titlebar { crate::titlebar::MAC_INSET } else { 6.0 });
|
||||
egui::MenuBar::new().config(egui::containers::menu::MenuConfig::new().style(crate::widgets::menu_style)).ui(ui, |ui| {
|
||||
for node in effectcraft_engine::menus::menu_bar() {
|
||||
if let MenuNode::Submenu { label, children } = node {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use craft_ui::theme::catppuccin::Flavor;
|
||||
use egui::{Color32, FontData, FontDefinitions, FontFamily, FontId, Stroke, TextStyle, Visuals};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -15,15 +16,29 @@ pub enum ThemeKind {
|
||||
Dark,
|
||||
Darker,
|
||||
Light,
|
||||
CatppuccinLatte,
|
||||
CatppuccinFrappe,
|
||||
CatppuccinMacchiato,
|
||||
CatppuccinMocha,
|
||||
}
|
||||
|
||||
impl ThemeKind {
|
||||
pub const ALL: [ThemeKind; 7] = [
|
||||
ThemeKind::Dark,
|
||||
ThemeKind::Darker,
|
||||
ThemeKind::Light,
|
||||
ThemeKind::CatppuccinLatte,
|
||||
ThemeKind::CatppuccinFrappe,
|
||||
ThemeKind::CatppuccinMacchiato,
|
||||
ThemeKind::CatppuccinMocha,
|
||||
];
|
||||
|
||||
pub fn from_name(s: &str) -> Option<ThemeKind> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"dark" | "default" => Some(ThemeKind::Dark),
|
||||
"darker" | "darkest" => Some(ThemeKind::Darker),
|
||||
"light" => Some(ThemeKind::Light),
|
||||
_ => None,
|
||||
let s = s.to_ascii_lowercase();
|
||||
match s.as_str() {
|
||||
"default" => Some(ThemeKind::Dark),
|
||||
"darkest" => Some(ThemeKind::Darker),
|
||||
_ => Self::ALL.into_iter().find(|k| k.name().eq_ignore_ascii_case(&s)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +48,26 @@ impl ThemeKind {
|
||||
ThemeKind::Dark => "dark",
|
||||
ThemeKind::Darker => "darker",
|
||||
ThemeKind::Light => "light",
|
||||
ThemeKind::CatppuccinLatte => "catppuccinLatte",
|
||||
ThemeKind::CatppuccinFrappe => "catppuccinFrappe",
|
||||
ThemeKind::CatppuccinMacchiato => "catppuccinMacchiato",
|
||||
ThemeKind::CatppuccinMocha => "catppuccinMocha",
|
||||
}
|
||||
}
|
||||
|
||||
/// The Catppuccin flavour behind this theme, if it is one.
|
||||
pub fn flavor(self) -> Option<Flavor> {
|
||||
match self {
|
||||
ThemeKind::CatppuccinLatte => Some(Flavor::Latte),
|
||||
ThemeKind::CatppuccinFrappe => Some(Flavor::Frappe),
|
||||
ThemeKind::CatppuccinMacchiato => Some(Flavor::Macchiato),
|
||||
ThemeKind::CatppuccinMocha => Some(Flavor::Mocha),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_light(self) -> bool {
|
||||
self == ThemeKind::Light
|
||||
matches!(self, ThemeKind::Light | ThemeKind::CatppuccinLatte)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +157,59 @@ impl Tokens {
|
||||
}
|
||||
}
|
||||
|
||||
/// A Catppuccin flavour (accent Mauve), over `base`'s metrics and label colours.
|
||||
fn catppuccin(base: Tokens, kind: ThemeKind, f: Flavor) -> Tokens {
|
||||
let p = f.palette();
|
||||
Tokens {
|
||||
kind,
|
||||
app_bg: p.crust,
|
||||
header_bg: p.mantle,
|
||||
panel_bg: p.base,
|
||||
tab_text: p.subtext0,
|
||||
tab_text_active: p.text,
|
||||
focus: p.mauve,
|
||||
accent: p.mauve,
|
||||
accent_hover: p.lavender,
|
||||
text: p.text,
|
||||
text_dim: p.subtext0,
|
||||
text_faint: p.overlay1,
|
||||
icon: p.subtext1,
|
||||
icon_active: p.text,
|
||||
hover: p.surface0,
|
||||
pressed: p.surface1,
|
||||
field_bg: p.mantle,
|
||||
field_border: p.surface1,
|
||||
separator: p.surface0,
|
||||
row: p.base,
|
||||
row_alt: p.mantle,
|
||||
row_selected: p.surface1,
|
||||
hot_text: p.mauve,
|
||||
tl_bg: p.mantle,
|
||||
tl_ruler_bg: p.base,
|
||||
tl_ruler_tick: p.overlay0,
|
||||
tl_ruler_text: p.subtext0,
|
||||
cti: p.mauve,
|
||||
work_area: p.surface1,
|
||||
cache_green: p.green,
|
||||
cache_blue: p.blue,
|
||||
keyframe: p.subtext0,
|
||||
keyframe_selected: p.mauve,
|
||||
pasteboard: p.crust,
|
||||
timecode: p.mauve,
|
||||
danger: p.red,
|
||||
warning: p.peach,
|
||||
fx_header: p.surface0,
|
||||
fx_header_active: p.surface1,
|
||||
tl_graph_selected: p.surface0,
|
||||
switch_well: p.mantle,
|
||||
tl_nav_bar: p.surface2,
|
||||
tl_work_area_bar: p.overlay0,
|
||||
expr_bg: p.mantle,
|
||||
expr_text: p.peach,
|
||||
..base
|
||||
}
|
||||
}
|
||||
|
||||
impl Tokens {
|
||||
pub fn for_kind(kind: ThemeKind) -> Self {
|
||||
let dark = Tokens {
|
||||
@@ -183,8 +266,11 @@ impl Tokens {
|
||||
row_h: 19.0,
|
||||
gradients: true,
|
||||
};
|
||||
if let Some(f) = kind.flavor() {
|
||||
return catppuccin(dark, kind, f);
|
||||
}
|
||||
match kind {
|
||||
ThemeKind::Dark => dark,
|
||||
ThemeKind::Dark | ThemeKind::CatppuccinLatte | ThemeKind::CatppuccinFrappe | ThemeKind::CatppuccinMacchiato | ThemeKind::CatppuccinMocha => dark,
|
||||
ThemeKind::Darker => Tokens {
|
||||
app_bg: Color32::from_rgb(0x08, 0x08, 0x08),
|
||||
header_bg: Color32::from_rgb(0x16, 0x16, 0x16),
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
//! The window's own title bar on Windows and Linux, where the window has no OS decorations
|
||||
//! ([`EffectcraftApp::custom_titlebar`]): the menu bar row is the title bar. Empty space in it
|
||||
//! drags the window and a double-click there maximizes it, caption buttons at the far right
|
||||
//! minimize, maximize/restore and close, and thin invisible zones along the window's edges resize
|
||||
//! it. On macOS the traffic lights sit over the same row ([`MAC_INSET`]) and only the drag works.
|
||||
|
||||
use egui::{CursorIcon, Id, LayerId, Order, PointerButton, Rect, ResizeDirection, Sense, Stroke, Ui, ViewportCommand, pos2, vec2};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::EffectcraftApp;
|
||||
use crate::i18n::tr;
|
||||
|
||||
/// One caption button: as wide as the native Windows ones.
|
||||
pub const BUTTON_WIDTH: f32 = 46.0;
|
||||
/// The three caption buttons together.
|
||||
pub const WIDTH: f32 = 3.0 * BUTTON_WIDTH;
|
||||
/// Height of the title row (menu bar) when it carries the caption buttons.
|
||||
pub const HEIGHT: f32 = 32.0;
|
||||
/// Room for the macOS traffic lights at the row's left end.
|
||||
pub const MAC_INSET: f32 = 80.0;
|
||||
/// Resize zone thickness along an edge, and the side of a corner zone.
|
||||
const EDGE: f32 = 6.0;
|
||||
const CORNER: f32 = 12.0;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
enum Caption {
|
||||
Minimize,
|
||||
Maximize,
|
||||
Close,
|
||||
}
|
||||
|
||||
/// Left to right.
|
||||
const CAPTIONS: [Caption; 3] = [Caption::Minimize, Caption::Maximize, Caption::Close];
|
||||
|
||||
/// Caption button ids (stable, so tests and agents can find them).
|
||||
fn caption_id(c: Caption) -> Id {
|
||||
Id::new(("titlebar-caption", c as u8))
|
||||
}
|
||||
|
||||
fn maximized(ctx: &egui::Context) -> bool {
|
||||
ctx.input(|i| i.viewport().maximized.unwrap_or(false))
|
||||
}
|
||||
|
||||
fn toggle_maximize(ctx: &egui::Context) {
|
||||
ctx.send_viewport_cmd(ViewportCommand::Maximized(!maximized(ctx)));
|
||||
}
|
||||
|
||||
/// The bar's background: a press-and-drag on empty space moves the window, a double-click toggles
|
||||
/// maximize. Call it before laying out the bar so its widgets, registered later, take the clicks.
|
||||
pub fn drag_area(ui: &Ui, rect: Rect) {
|
||||
let ctx = ui.ctx();
|
||||
let resp = ui.interact(rect, Id::new("titlebar-drag"), Sense::click_and_drag());
|
||||
// egui hands a drag that starts on a click-only widget (a menu title) to the drag-sensing
|
||||
// background below it, so remember whether the press itself landed on bare background.
|
||||
if ctx.input(|i| i.pointer.any_pressed()) {
|
||||
let bare = ctx.viewport(|v| v.hits.click.is_some_and(|w| w.id == resp.id));
|
||||
ctx.data_mut(|d| d.insert_temp(resp.id, bare));
|
||||
}
|
||||
if resp.double_clicked() {
|
||||
toggle_maximize(ctx);
|
||||
} else if resp.drag_started_by(PointerButton::Primary) && ctx.data(|d| d.get_temp(resp.id).unwrap_or(false)) {
|
||||
ctx.send_viewport_cmd(ViewportCommand::StartDrag);
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimize, Maximize/Restore and Close at the right end of `bar` (the title row's outer rect),
|
||||
/// full row height, flush with the window's edge so a maximized window's corner hits Close. Close
|
||||
/// runs `app.quit`, which asks about unsaved changes first.
|
||||
pub fn caption_buttons(app: &mut EffectcraftApp, ui: &mut Ui, bar: Rect) {
|
||||
let t = app.tokens;
|
||||
let max = maximized(ui.ctx());
|
||||
let mut clicked = None;
|
||||
for (i, c) in CAPTIONS.into_iter().enumerate() {
|
||||
let r = Rect::from_min_size(pos2(bar.right() - WIDTH + i as f32 * BUTTON_WIDTH, bar.top()), vec2(BUTTON_WIDTH, bar.height() - 1.0));
|
||||
let resp = ui.interact(r, caption_id(c), Sense::click());
|
||||
let close = c == Caption::Close;
|
||||
let fill = match (resp.is_pointer_button_down_on(), resp.hovered()) {
|
||||
(true, _) if close => t.danger.gamma_multiply(0.8),
|
||||
(_, true) if close => t.danger,
|
||||
(true, _) => t.pressed,
|
||||
(_, true) => t.hover,
|
||||
_ => egui::Color32::TRANSPARENT,
|
||||
};
|
||||
ui.painter().rect_filled(r, 0.0, fill);
|
||||
let ink = if close && resp.hovered() { egui::Color32::WHITE } else { t.icon };
|
||||
paint_glyph(ui, c, max, r.center(), Stroke::new(1.0, ink));
|
||||
let tip = match c {
|
||||
Caption::Minimize => tr("Minimize"),
|
||||
Caption::Maximize if max => tr("Restore"),
|
||||
Caption::Maximize => tr("Maximize"),
|
||||
Caption::Close => tr("Close"),
|
||||
};
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tip));
|
||||
if resp.on_hover_text(tip).clicked() {
|
||||
clicked = Some(c);
|
||||
}
|
||||
}
|
||||
match clicked {
|
||||
Some(Caption::Minimize) => ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true)),
|
||||
Some(Caption::Maximize) => toggle_maximize(ui.ctx()),
|
||||
Some(Caption::Close) => {
|
||||
let ctx = ui.ctx().clone();
|
||||
if let Err(e) = crate::menus::invoke(app, &ctx, "app.quit", json!({})) {
|
||||
app.ui.status = e;
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The 10 pt line glyphs of the caption buttons.
|
||||
fn paint_glyph(ui: &Ui, c: Caption, maximized: bool, at: egui::Pos2, s: Stroke) {
|
||||
let p = ui.painter();
|
||||
let g = Rect::from_center_size(at, vec2(10.0, 10.0));
|
||||
match c {
|
||||
Caption::Minimize => {
|
||||
p.line_segment([g.left_center(), g.right_center()], s);
|
||||
}
|
||||
// Restore: a front square with the corner of the one behind it.
|
||||
Caption::Maximize if maximized => {
|
||||
let front = Rect::from_min_max(g.min + vec2(0.0, 2.0), g.max - vec2(2.0, 0.0));
|
||||
p.rect_stroke(front, 0.0, s, egui::StrokeKind::Middle);
|
||||
p.line(vec![g.min + vec2(2.0, 2.0), g.min + vec2(2.0, 0.0), g.right_top(), g.max - vec2(0.0, 2.0), g.max - vec2(2.0, 2.0)], s);
|
||||
}
|
||||
Caption::Maximize => {
|
||||
p.rect_stroke(g, 0.0, s, egui::StrokeKind::Middle);
|
||||
}
|
||||
Caption::Close => {
|
||||
p.line_segment([g.left_top(), g.right_bottom()], s);
|
||||
p.line_segment([g.right_top(), g.left_bottom()], s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The edge and corner zones of a window with content rect `w`: (zone, direction, cursor). Corners
|
||||
/// come last so they win where they overlap the edges.
|
||||
fn zones(w: Rect) -> [(Rect, ResizeDirection, CursorIcon); 8] {
|
||||
use ResizeDirection::*;
|
||||
let (e, c) = (EDGE, CORNER);
|
||||
[
|
||||
(Rect::from_min_max(w.left_top(), pos2(w.right(), w.top() + e)), North, CursorIcon::ResizeVertical),
|
||||
(Rect::from_min_max(pos2(w.left(), w.bottom() - e), w.right_bottom()), South, CursorIcon::ResizeVertical),
|
||||
(Rect::from_min_max(w.left_top(), pos2(w.left() + e, w.bottom())), West, CursorIcon::ResizeHorizontal),
|
||||
(Rect::from_min_max(pos2(w.right() - e, w.top()), w.right_bottom()), East, CursorIcon::ResizeHorizontal),
|
||||
(Rect::from_min_size(w.left_top(), vec2(c, c)), NorthWest, CursorIcon::ResizeNwSe),
|
||||
(Rect::from_min_size(w.right_top() - vec2(c, 0.0), vec2(c, c)), NorthEast, CursorIcon::ResizeNeSw),
|
||||
(Rect::from_min_size(w.left_bottom() - vec2(0.0, c), vec2(c, c)), SouthWest, CursorIcon::ResizeNeSw),
|
||||
(Rect::from_min_size(w.right_bottom() - vec2(c, c), vec2(c, c)), SouthEast, CursorIcon::ResizeNwSe),
|
||||
]
|
||||
}
|
||||
|
||||
/// Invisible resize zones along the window's edges, above everything else; a press in one starts
|
||||
/// an OS resize in that direction. None while the window is maximized or fullscreen.
|
||||
pub fn resize_zones(ui: &mut Ui) {
|
||||
let ctx = ui.ctx().clone();
|
||||
if maximized(&ctx) || ctx.input(|i| i.viewport().fullscreen.unwrap_or(false)) {
|
||||
return;
|
||||
}
|
||||
let w = ctx.content_rect();
|
||||
let layer = LayerId::new(Order::Foreground, Id::new("titlebar-resize"));
|
||||
let zui = ui.new_child(egui::UiBuilder::new().layer_id(layer).max_rect(w));
|
||||
for (i, (r, dir, cursor)) in zones(w).into_iter().enumerate() {
|
||||
let resp = zui.interact(r, Id::new(("titlebar-resize", i)), Sense::drag()).on_hover_cursor(cursor);
|
||||
if resp.drag_started_by(PointerButton::Primary) {
|
||||
ctx.send_viewport_cmd(ViewportCommand::BeginResize(dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use egui::{Event, Pos2, RawInput, ViewportId};
|
||||
|
||||
/// A headless window `width` pt wide with a title row: the drag area, the caption buttons and
|
||||
/// the resize zones.
|
||||
struct Win {
|
||||
ctx: egui::Context,
|
||||
app: EffectcraftApp,
|
||||
width: f32,
|
||||
time: f64,
|
||||
maximized: bool,
|
||||
}
|
||||
|
||||
impl Win {
|
||||
fn new(width: f32) -> Self {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
app.custom_titlebar = true;
|
||||
app.apply_prefs(&ctx);
|
||||
Self { ctx, app, width, time: 0.0, maximized: false }
|
||||
}
|
||||
|
||||
fn frame(&mut self, events: Vec<Event>) -> egui::FullOutput {
|
||||
self.time += 0.05;
|
||||
let mut input =
|
||||
RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(self.width, 600.0))), time: Some(self.time), events, ..Default::default() };
|
||||
if let Some(v) = input.viewports.get_mut(&ViewportId::ROOT) {
|
||||
v.maximized = Some(self.maximized);
|
||||
}
|
||||
let app = &mut self.app;
|
||||
let width = self.width;
|
||||
let mut out = self.ctx.run_ui(input, |ui| {
|
||||
let bar = Rect::from_min_size(Pos2::ZERO, vec2(width, HEIGHT));
|
||||
drag_area(ui, bar);
|
||||
caption_buttons(app, ui, bar);
|
||||
resize_zones(ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
out
|
||||
}
|
||||
|
||||
fn gesture(&mut self, path: &[Pos2]) -> Vec<ViewportCommand> {
|
||||
let button = |p: Pos2, pressed| Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
let mut events = vec![vec![Event::PointerMoved(path[0])], vec![button(path[0], true)]];
|
||||
events.extend(path[1..].iter().map(|p| vec![Event::PointerMoved(*p)]));
|
||||
events.push(vec![button(*path.last().unwrap_or(&path[0]), false)]);
|
||||
events.into_iter().flat_map(|e| self.frame(e).viewport_output.remove(&ViewportId::ROOT).map(|v| v.commands).unwrap_or_default()).collect()
|
||||
}
|
||||
|
||||
fn click(&mut self, p: Pos2) -> Vec<ViewportCommand> {
|
||||
self.gesture(&[p])
|
||||
}
|
||||
|
||||
fn caption(&self, c: Caption) -> Rect {
|
||||
self.ctx.read_response(caption_id(c)).map(|r| r.rect).unwrap_or(Rect::NOTHING)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caption_buttons_take_the_right_end_of_the_row() {
|
||||
for width in [800.0, 1440.0] {
|
||||
let mut w = Win::new(width);
|
||||
w.frame(vec![]);
|
||||
w.frame(vec![]);
|
||||
let [min, max, close] = CAPTIONS.map(|c| w.caption(c));
|
||||
assert_eq!((close.right(), close.top()), (width, 0.0), "Close sits in the window's corner at {width}");
|
||||
assert!(close.height() >= 30.0 && [min, max, close].iter().all(|r| r.width() == BUTTON_WIDTH));
|
||||
assert_eq!((min.right(), max.right()), (max.left(), close.left()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimize_maximize_and_close_follow_the_window_state() {
|
||||
let mut w = Win::new(1000.0);
|
||||
w.frame(vec![]);
|
||||
let (min, max, close) = (w.caption(Caption::Minimize).center(), w.caption(Caption::Maximize).center(), w.caption(Caption::Close).center());
|
||||
assert!(w.click(min).contains(&ViewportCommand::Minimized(true)));
|
||||
assert!(w.click(max).contains(&ViewportCommand::Maximized(true)));
|
||||
w.maximized = true;
|
||||
assert!(w.click(max).contains(&ViewportCommand::Maximized(false)), "Restore");
|
||||
// Close goes through app.quit (which asks about unsaved changes) rather than the OS.
|
||||
assert!(w.click(close).contains(&ViewportCommand::Close));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_row_space_drags_and_double_click_maximizes() {
|
||||
let mut w = Win::new(1440.0);
|
||||
w.frame(vec![]);
|
||||
let empty = pos2(700.0, 16.0);
|
||||
let cmds = w.gesture(&[empty, empty + vec2(12.0, 4.0), empty + vec2(30.0, 8.0)]);
|
||||
assert_eq!(cmds.iter().filter(|c| matches!(c, ViewportCommand::StartDrag)).count(), 1, "{cmds:?}");
|
||||
let mut cmds = w.click(empty);
|
||||
cmds.extend(w.click(empty));
|
||||
assert!(cmds.contains(&ViewportCommand::Maximized(true)), "{cmds:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_zones_resize_unless_maximized() {
|
||||
use ResizeDirection::*;
|
||||
let mut w = Win::new(1000.0);
|
||||
w.frame(vec![]);
|
||||
for (p, dir) in [
|
||||
(pos2(1.0, 300.0), West),
|
||||
(pos2(998.0, 300.0), East),
|
||||
(pos2(500.0, 1.0), North),
|
||||
(pos2(500.0, 598.0), South),
|
||||
(pos2(2.0, 2.0), NorthWest),
|
||||
(pos2(997.0, 3.0), NorthEast),
|
||||
(pos2(3.0, 597.0), SouthWest),
|
||||
(pos2(998.0, 598.0), SouthEast),
|
||||
] {
|
||||
let cmds = w.gesture(&[p, p + vec2(6.0, 6.0)]);
|
||||
assert_eq!(cmds, vec![ViewportCommand::BeginResize(dir)], "at {p:?}");
|
||||
}
|
||||
w.maximized = true;
|
||||
let cmds = w.gesture(&[pos2(1.0, 300.0), pos2(8.0, 300.0)]);
|
||||
assert!(!cmds.iter().any(|c| matches!(c, ViewportCommand::BeginResize(_))), "{cmds:?}");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The toolbar's Fill / Stroke swatch with Linear or Radial Gradient (#612, reported on Discord):
|
||||
//! The toolbar's Fill / Stroke swatch with Linear or Radial Gradient (#612, reported by a user):
|
||||
//! it opens the Gradient Editor on the selected shape layer's Gradient Fill / Gradient Stroke
|
||||
//! Colors, as in After Effects, instead of Fill / Stroke Options. The words still open the
|
||||
//! options, a Solid Color swatch the colour picker, Alt-click still steps through the kinds, and
|
||||
|
||||
@@ -432,7 +432,7 @@ fn timeline_selected_effect_row_highlights() {
|
||||
|
||||
/// Right-clicking a layer in the Timeline keeps the selection when the layer is part of it,
|
||||
/// so the context menu's commands act on every selected layer; right-clicking an unselected
|
||||
/// layer selects only it (After Effects). Reported on Discord: Pre-compose and Delete from the
|
||||
/// layer selects only it (After Effects). Reported by a user: Pre-compose and Delete from the
|
||||
/// menu acted on the right-clicked layer only (#878).
|
||||
#[test]
|
||||
fn timeline_context_menu_acts_on_the_whole_selection() {
|
||||
|
||||
+1
-1
@@ -145,7 +145,7 @@ The most important missing piece: it turns every other estimate here into a meas
|
||||
tests, checked headlessly on Windows, drag a selected Gradient Fill's start and end points in
|
||||
the Composition viewer: one undo step, the layer stays, a point inside a moved and scaled group
|
||||
converts to the group's space, and the handles show only for a selected gradient.
|
||||
- Regression evidence for the toolbar's gradient swatches (#612, reported on Discord): the
|
||||
- Regression evidence for the toolbar's gradient swatches (#612, reported by a user): the
|
||||
`ui_toolbar_gradient` input tests, checked headlessly on Windows, click a Linear Fill swatch and
|
||||
a Radial Stroke swatch on a selected shape layer and get the Gradient Editor on its Gradient Fill
|
||||
/ Gradient Stroke Colors (OK saves); without such a gradient the swatch still opens the options,
|
||||
|
||||
+7
-4
@@ -55,10 +55,10 @@ Commands (CLI, MCP, control channel):
|
||||
background, from the current time on, until it is cached or the budget is full
|
||||
- `previews.fastPreviews`: Fast Previews (Draft 3D, Faster Effects)
|
||||
- `appearance.appearanceMode`: Appearance Mode (`auto` / `dark` / `light`). `auto` (Sync with System) follows the operating system's light or dark appearance while the app runs: on Linux the desktop app reads the XDG desktop portal (`org.freedesktop.appearance` `color-scheme`) once and then listens for its `SettingChanged` signal on a worker thread, repainting only when the value changes (nothing polls); `gsettings` runs at most once at start-up, by absolute path, when the portal gives no answer. Elsewhere it uses what the window system reports. With no answer it shows the dark theme. New installs stay `dark`; Sync with System is opt-in. The Tools bar's appearance button (a monitor, sun or moon) cycles Sync with System, Light and Dark (`view.theme.toggle`, Next Appearance Mode).
|
||||
- `appearance.darkTheme`: Dark Theme (`dark` / `darker`), shown in dark appearance
|
||||
- `appearance.lightTheme`: Light Theme (`light`), shown in light appearance. Settings ▸ Appearance shows the mode above a light and a dark theme card, each with a preview of the EffectCraft window in that theme.
|
||||
- `appearance.favoriteThemes`: a list of unique theme ids (`dark`, `darker`, `light`), empty by default. Hover a theme row to star it; saved favorites keep an accent-colored star. Right-click the appearance button for Appearance Settings and favorites grouped light first, then dark. Selecting a favorite fixes its appearance family and preserves the opposite family choice; left-click still cycles modes. Settings OK saves favorites and Cancel restores them. Unknown or duplicate ids are rejected by `prefs.set` and removed when loading old settings.
|
||||
- `appearance.theme`: the single theme from before appearance modes, kept for older settings files and automation. A saved file with only `theme` loads as a fixed Dark or Light mode showing it; setting it (`prefs.set`, `ui.set {"theme": …}`, `view.theme.dark|darker|light`) selects that theme and fixes the mode to its family. While the mode is Dark or Light it mirrors the theme in use.
|
||||
- `appearance.darkTheme`: Dark Theme (`dark`, `darker`, `catppuccinFrappe`, `catppuccinMacchiato` or `catppuccinMocha`), shown in dark appearance
|
||||
- `appearance.lightTheme`: Light Theme (`light` or `catppuccinLatte`), shown in light appearance. Settings ▸ Appearance shows the mode above a light and a dark theme card, each with a preview of the EffectCraft window in that theme.
|
||||
- `appearance.favoriteThemes`: a list of unique theme ids (any theme id, such as `dark`, `darker`, `light` or `catppuccinMocha`), empty by default. Hover a theme row to star it; saved favorites keep an accent-colored star. Right-click the appearance button for Appearance Settings and favorites grouped light first, then dark. Selecting a favorite fixes its appearance family and preserves the opposite family choice; left-click still cycles modes. Settings OK saves favorites and Cancel restores them. Unknown or duplicate ids are rejected by `prefs.set` and removed when loading old settings.
|
||||
- `appearance.theme`: the single theme from before appearance modes, kept for older settings files and automation. A saved file with only `theme` loads as a fixed Dark or Light mode showing it; setting it (`prefs.set`, `ui.set {"theme": …}`, `view.theme.dark|darker|light|catppuccinLatte|catppuccinFrappe|catppuccinMacchiato|catppuccinMocha`; View ▸ Preferences ▸ Theme) selects that theme and fixes the mode to its family. While the mode is Dark or Light it mirrors the theme in use.
|
||||
- `appearance.brightness`: Brightness
|
||||
- `appearance.uiScale`: UI Scale, the size of the whole interface (any whole percentage from
|
||||
75 to 200, on top of the display's own scaling), dragged or typed; it applies as soon as it is
|
||||
@@ -106,6 +106,9 @@ Commands (CLI, MCP, control channel):
|
||||
- `appearance.useGradients`: soft gradient on panel tab strips
|
||||
- `appearance.inWindowMenuBarMac`: on macOS, draw the menu bar inside the window instead of the
|
||||
native menu bar
|
||||
- `appearance.systemTitleBar`: on Windows and Linux, keep the system's title bar instead of the
|
||||
window's own (the menu bar row with minimize, maximize and close buttons); applies from the next
|
||||
launch
|
||||
- `grids.gridStyle`, `grids.guideStyle`: lines, dashed lines or dots (grid, proportional grid,
|
||||
guides); `grids.proportionalHorizontal` / `grids.proportionalVertical`: View ▸ Show
|
||||
Proportional Grid divisions
|
||||
|
||||
Reference in New Issue
Block a user