Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7838dd4189 | |||
| f7d8065119 | |||
| eb86a40890 | |||
| 31e66b0606 | |||
| 40b97b359a |
Generated
+37
-37
@@ -965,7 +965,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-ui"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=5c874d51eeb3de70e735c8ce87a59a7dedd47675#5c874d51eeb3de70e735c8ce87a59a7dedd47675"
|
||||
source = "git+https://git.marcospaulo.dev.br/masi/craft-libs?rev=22544b6d619d4f16779b8bdfa3f0b642a86fc065#22544b6d619d4f16779b8bdfa3f0b642a86fc065"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"serde",
|
||||
@@ -1143,7 +1143,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"effectcraft-engine",
|
||||
@@ -1167,7 +1167,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-automation"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-engine",
|
||||
"effectcraft-host",
|
||||
@@ -1179,14 +1179,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-av1enc"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"filmcraft-av1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-cli"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"cpu-time",
|
||||
"effectcraft-automation",
|
||||
@@ -1202,7 +1202,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-color"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -1210,7 +1210,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-effects"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-color",
|
||||
"effectcraft-geom",
|
||||
@@ -1227,7 +1227,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-engine"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-color",
|
||||
"effectcraft-effects",
|
||||
@@ -1260,7 +1260,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-export"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-av1enc",
|
||||
"effectcraft-color",
|
||||
@@ -1290,7 +1290,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-expr"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"boa_engine",
|
||||
"effectcraft-color",
|
||||
@@ -1307,7 +1307,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-geom"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -1315,7 +1315,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-gpu"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"effectcraft-color",
|
||||
@@ -1337,14 +1337,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-hevcenc"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"filmcraft-hevc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-host"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-engine",
|
||||
"effectcraft-export",
|
||||
@@ -1368,7 +1368,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-interchange"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-color",
|
||||
"effectcraft-keyframe",
|
||||
@@ -1387,7 +1387,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-keyframe"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-color",
|
||||
"effectcraft-geom",
|
||||
@@ -1399,7 +1399,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-lottie"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-color",
|
||||
"effectcraft-effects",
|
||||
@@ -1416,7 +1416,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-media"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-model",
|
||||
"effectcraft-pdf",
|
||||
@@ -1442,7 +1442,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-model"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-geom",
|
||||
"image",
|
||||
@@ -1453,14 +1453,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-opusenc"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"filmcraft-opus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-path"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-geom",
|
||||
"effectcraft-keyframe",
|
||||
@@ -1472,7 +1472,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-pdf"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-raster",
|
||||
"effectcraft-svg",
|
||||
@@ -1486,7 +1486,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-plugin"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-effects",
|
||||
"effectcraft-keyframe",
|
||||
@@ -1498,7 +1498,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-project"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-color",
|
||||
"effectcraft-geom",
|
||||
@@ -1511,14 +1511,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-psd"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"miniz_oxide 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-raster"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-color",
|
||||
"effectcraft-geom",
|
||||
@@ -1527,7 +1527,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-render"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-color",
|
||||
"effectcraft-effects",
|
||||
@@ -1549,7 +1549,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-script"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"boa_engine",
|
||||
"effectcraft-engine",
|
||||
@@ -1564,7 +1564,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-segment"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"miniz_oxide 0.8.9",
|
||||
"ndarray",
|
||||
@@ -1573,7 +1573,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-svg"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-geom",
|
||||
"effectcraft-path",
|
||||
@@ -1585,7 +1585,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-text"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-keyframe",
|
||||
"harfrust",
|
||||
@@ -1597,7 +1597,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-time"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -1605,7 +1605,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-track"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-raster",
|
||||
"effectcraft-segment",
|
||||
@@ -1617,7 +1617,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-ui-egui"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"craft-ui",
|
||||
"effectcraft-color",
|
||||
@@ -1649,11 +1649,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-vp9enc"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-web"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"effectcraft-engine",
|
||||
"effectcraft-export",
|
||||
@@ -6463,7 +6463,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -99,7 +99,7 @@ pollster = "0.4"
|
||||
|
||||
# ui
|
||||
# Shared frontend components; keep this dependency above the engine boundary.
|
||||
craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] }
|
||||
craft-ui = { version = "0.1.0", git = "https://git.marcospaulo.dev.br/masi/craft-libs", rev = "22544b6d619d4f16779b8bdfa3f0b642a86fc065", features = ["serde"] }
|
||||
egui = "0.36"
|
||||
eframe = { version = "0.36", default-features = false }
|
||||
egui_kittest = "0.36"
|
||||
|
||||
@@ -44,6 +44,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()
|
||||
@@ -105,12 +120,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([1680.0, 1020.0])
|
||||
.with_min_inner_size([960.0, 600.0])
|
||||
.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())
|
||||
@@ -190,6 +210,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 {
|
||||
@@ -400,7 +422,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)
|
||||
|
||||
@@ -35,6 +35,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",
|
||||
|
||||
@@ -1138,6 +1138,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
|
||||
|
||||
@@ -45,13 +45,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)]> {
|
||||
@@ -185,6 +199,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 {
|
||||
@@ -1178,6 +1195,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 {
|
||||
|
||||
@@ -384,6 +384,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...", "合成设置..."),
|
||||
@@ -1005,6 +1014,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...", "合成設定..."),
|
||||
@@ -1623,6 +1641,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...", "コンポジション設定..."),
|
||||
@@ -2244,6 +2271,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...", "Налаштування композиції..."),
|
||||
@@ -2866,6 +2902,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..."),
|
||||
@@ -3486,6 +3531,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..."),
|
||||
|
||||
@@ -505,6 +505,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)] = &[
|
||||
@@ -1025,6 +1030,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)] = &[
|
||||
@@ -1540,6 +1550,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.
|
||||
@@ -1972,6 +1987,11 @@ const UKRAINIAN: &[(&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"),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1984,6 +2004,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")),
|
||||
|
||||
@@ -29,6 +29,7 @@ mod pointer_wrap;
|
||||
pub mod prefs_live;
|
||||
pub mod state;
|
||||
pub mod theme;
|
||||
pub mod titlebar;
|
||||
pub mod widgets;
|
||||
mod workspace_state;
|
||||
|
||||
@@ -315,7 +316,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`).
|
||||
@@ -405,6 +409,7 @@ impl EffectcraftApp {
|
||||
pointer_comp: None,
|
||||
dialog_state: Default::default(),
|
||||
integrated_titlebar: false,
|
||||
custom_titlebar: false,
|
||||
audio: None,
|
||||
scrub: None,
|
||||
meter: Default::default(),
|
||||
@@ -1615,15 +1620,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);
|
||||
@@ -1640,6 +1658,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();
|
||||
|
||||
@@ -47,10 +47,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),
|
||||
@@ -1769,7 +1765,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().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:?}");
|
||||
}
|
||||
}
|
||||
+6
-3
@@ -55,9 +55,9 @@ 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.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.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 (75–200 %, on top of the
|
||||
display's own scaling), applied as soon as it is chosen
|
||||
@@ -102,6 +102,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
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
Produces, in $env:DIST (default: dist/release):
|
||||
effectcraft-<version>-windows-<arch>.msi per-machine installer (WiX v5)
|
||||
effectcraft-<version>-windows-<arch>-portable.zip effectcraft.exe + effectcraft-cli.exe
|
||||
effectcraft-cli-<version>-windows-<arch>.zip effectcraft-cli.exe standalone
|
||||
|
||||
The binaries link the C runtime statically (+crt-static), so neither the MSI nor the portable
|
||||
zip needs the Visual C++ redistributable. Signing is delegated to sign.ps1 (skipped with a
|
||||
@@ -25,6 +26,7 @@ param(
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
Set-Location $Root
|
||||
|
||||
function Invoke-Native([string] $What, [scriptblock] $Block) {
|
||||
Write-Output "==> $What"
|
||||
@@ -56,6 +58,17 @@ if (-not $env:EFFECTCRAFT_BUILD_DATE) { $env:EFFECTCRAFT_BUILD_DATE = (Get-Date)
|
||||
Write-Output "EffectCraft $Version for Windows $Arch ($Target)"
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
# Locate rc.exe if not in PATH
|
||||
if (-not (Get-Command rc.exe -ErrorAction SilentlyContinue)) {
|
||||
$kits = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
|
||||
$foundRc = Get-ChildItem -Path $kits -Recurse -Filter rc.exe -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -match '\\x64\\rc\.exe$' } |
|
||||
Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($foundRc) {
|
||||
$env:PATH = "$($foundRc.DirectoryName);$env:PATH"
|
||||
}
|
||||
}
|
||||
|
||||
# Static CRT: no VC++ redistributable needed. Scoped to the target so host build scripts and
|
||||
# proc-macros are unaffected.
|
||||
$flagVar = 'CARGO_TARGET_' + ($Target.ToUpper() -replace '-', '_') + '_RUSTFLAGS'
|
||||
@@ -114,6 +127,19 @@ $Zip = Join-Path $Dist "effectcraft-$Version-windows-$Arch-portable.zip"
|
||||
Remove-Item -Force $Zip -ErrorAction SilentlyContinue
|
||||
Compress-Archive -Path $Portable -DestinationPath $Zip
|
||||
|
||||
# ---- cli zip -----------------------------------------------------------------------------------
|
||||
$CliPortable = Join-Path $TargetDir "windows-package\effectcraft-cli-$Version-windows-$Arch"
|
||||
Remove-Item -Recurse -Force $CliPortable -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $CliPortable | Out-Null
|
||||
Copy-Item (Join-Path $Stage 'effectcraft-cli.exe') $CliPortable
|
||||
foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE') {
|
||||
$p = Join-Path $Root $f
|
||||
if (Test-Path $p) { Copy-Item $p $CliPortable }
|
||||
}
|
||||
$CliZip = Join-Path $Dist "effectcraft-cli-$Version-windows-$Arch.zip"
|
||||
Remove-Item -Force $CliZip -ErrorAction SilentlyContinue
|
||||
Compress-Archive -Path $CliPortable -DestinationPath $CliZip
|
||||
|
||||
# Smoke-test the CLI when this machine can run it. An ARM64 build made on an x64 runner can't run
|
||||
# here; .github/workflows/windows-arm64.yml installs and runs it on ARM64 instead.
|
||||
$HostArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
|
||||
@@ -122,4 +148,4 @@ if ($Arch -ne 'arm64' -or $HostArch -eq 'arm64') {
|
||||
} else {
|
||||
Write-Output "skipping effectcraft-cli --version: an $Arch build doesn't run on this $HostArch machine"
|
||||
}
|
||||
Get-Item $Msi, $Zip | Format-Table Name, Length
|
||||
Get-Item $Msi, $Zip, $CliZip | Format-Table Name, Length
|
||||
|
||||
Reference in New Issue
Block a user