Files
vectorcraft/apps/drawcraft/src/main.rs
T

154 lines
5.6 KiB
Rust

//! DrawCraft desktop app.
//!
//! Usage: `drawcraft [--control <port>] [files…]`
//!
//! `--control <port>` (or `DRAWCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server:
//! `{"id":1,"method":"ui.inspect","params":{}}` → `{"id":1,"ok":true,"result":…}`.
//! See `drawcraft_ui_egui::control` for the methods.
mod control_server;
#[cfg(target_os = "macos")]
mod native_menu;
use drawcraft_engine::Session;
use drawcraft_ui_egui::{DrawcraftApp, Services};
struct App(DrawcraftApp, #[cfg(target_os = "macos")] Option<native_menu::NativeMenu>);
impl eframe::App for App {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
#[cfg(target_os = "macos")]
{
if self.1.is_none() && std::env::var_os("DRAWCRAFT_NO_NATIVE_MENU").is_none() {
self.1 = Some(native_menu::NativeMenu::install(&mut self.0));
}
if let Some(m) = &mut self.1 {
m.poll(&mut self.0);
}
}
self.0.logic(ctx);
if self.0.ui.status == "quit" {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
}
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
self.0.raw_input_hook(raw);
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.0.ui(ui);
}
fn on_exit(&mut self) {
save_prefs(&self.0);
}
}
/// Where UI preferences live: ~/Library/Application Support/DrawCraft (macOS),
/// %APPDATA%\DrawCraft (Windows), $XDG_CONFIG_HOME or ~/.config/drawcraft (Linux).
fn prefs_path() -> Option<std::path::PathBuf> {
let base = if cfg!(target_os = "macos") {
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join("Library/Application Support/DrawCraft"))
} else if cfg!(windows) {
std::env::var_os("APPDATA").map(|a| std::path::PathBuf::from(a).join("DrawCraft"))
} else {
std::env::var_os("XDG_CONFIG_HOME")
.map(std::path::PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config")))
.map(|c| c.join("drawcraft"))
};
base.map(|b| b.join("ui.json"))
}
fn load_prefs(app: &mut DrawcraftApp) {
if std::env::var_os("DRAWCRAFT_NO_PREFS").is_some() {
return;
}
if let Some(p) = prefs_path()
&& let Ok(bytes) = std::fs::read(&p)
&& let Ok(ui) = serde_json::from_slice::<drawcraft_ui_egui::UiState>(&bytes)
{
app.ui = ui.sanitized();
}
drawcraft_ui_egui::prefs_dialog::restore(app);
}
fn save_prefs(app: &DrawcraftApp) {
if std::env::var_os("DRAWCRAFT_NO_PREFS").is_some() {
return;
}
if let Some(p) = prefs_path() {
let _ = std::fs::create_dir_all(p.parent().unwrap_or(std::path::Path::new(".")));
let mut ui = app.ui.clone();
ui.engine_prefs = app.session.prefs.to_json();
if let Ok(bytes) = serde_json::to_vec_pretty(&ui) {
let _ = std::fs::write(p, bytes);
}
}
}
fn services() -> Services {
Services {
pick_open: Some(Box::new(|| {
rfd::FileDialog::new()
.add_filter("All supported", &["drawcraft", "svg", "png", "jpg", "jpeg", "gif", "webp"])
.pick_file()
.map(|p| p.to_string_lossy().to_string())
})),
pick_save: Some(Box::new(|name: &str| rfd::FileDialog::new().set_file_name(name).save_file().map(|p| p.to_string_lossy().to_string()))),
read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))),
write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))),
// Menu-bar Paste never sees egui's Paste event, so read the clipboard directly.
clipboard_read: Some(Box::new(|| arboard::Clipboard::new().ok()?.get_text().ok())),
..Default::default()
}
}
fn main() -> eframe::Result {
let mut control_port: Option<u16> = std::env::var("DRAWCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok());
let mut files = Vec::new();
let mut args = std::env::args().skip(1);
while let Some(a) = args.next() {
match a.as_str() {
"--control" => control_port = args.next().and_then(|p| p.parse().ok()),
"--version" => {
println!("drawcraft {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
_ => files.push(a),
}
}
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_title("DrawCraft")
.with_inner_size([1440.0, 900.0])
.with_min_inner_size([800.0, 500.0])
.with_drag_and_drop(true)
.with_fullsize_content_view(true)
.with_titlebar_shown(false)
.with_title_shown(false),
..Default::default()
};
eframe::run_native(
"DrawCraft",
options,
Box::new(move |cc| {
let mut app = DrawcraftApp::new(Session::new(), services());
load_prefs(&mut app);
app.integrated_titlebar = cfg!(target_os = "macos");
if let Some(port) = control_port {
let rx = control_server::start(port, cc.egui_ctx.clone());
app = app.with_control(rx);
}
for f in files {
if let Err(e) = drawcraft_ui_egui::io::open_path(&mut app, &f) {
eprintln!("drawcraft: {f}: {e}");
}
}
Ok(Box::new(App(
app,
#[cfg(target_os = "macos")]
None,
)))
}),
)
}