261 lines
10 KiB
Rust
261 lines
10 KiB
Rust
mod color_scheme;
|
|
mod composite_icon;
|
|
mod config;
|
|
mod icon_index;
|
|
mod ui;
|
|
|
|
use gtk::prelude::*;
|
|
use gtk::{glib, Application, ApplicationWindow, Stack};
|
|
use std::path::{Path, PathBuf};
|
|
use crate::color_scheme::ColorScheme;
|
|
use crate::composite_icon::CompositeIcon;
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
const APP_ID: &str = "dev.rootiest.IconColorTool";
|
|
|
|
fn main() -> glib::ExitCode {
|
|
let app = Application::builder().application_id(APP_ID).build();
|
|
|
|
app.connect_activate(|app| {
|
|
let breeze_path = match config::load_breeze_path() {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
eprintln!("Failed to resolve breeze-icons path: {e:#}");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
let initial_arg = std::env::args().nth(1);
|
|
let initial_icon = initial_arg
|
|
.as_deref()
|
|
.map(|a| resolve_icon_arg(a, &breeze_path));
|
|
|
|
if let Some(Err(e)) = &initial_icon {
|
|
eprintln!("Error: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let window = ApplicationWindow::builder()
|
|
.application(app)
|
|
.title("Icon Color Tool")
|
|
.default_width(900)
|
|
.default_height(800)
|
|
.build();
|
|
|
|
let stack = Stack::new();
|
|
window.set_child(Some(&stack));
|
|
|
|
let current_composite: Rc<RefCell<Option<Rc<RefCell<CompositeIcon>>>>> = Rc::new(RefCell::new(None));
|
|
|
|
{
|
|
let current_composite = Rc::clone(¤t_composite);
|
|
let window_clone = window.clone();
|
|
window.connect_close_request(move |_| {
|
|
if let Some(comp) = current_composite.borrow().as_ref() {
|
|
if comp.borrow().is_modified {
|
|
let dialog = gtk::AlertDialog::builder()
|
|
.message("You have unsaved changes. Are you sure you want to close?")
|
|
.detail("All unsaved changes will be lost.")
|
|
.buttons(["Cancel", "Discard"])
|
|
.cancel_button(0)
|
|
.default_button(1)
|
|
.build();
|
|
|
|
let window_clone2 = window_clone.clone();
|
|
let comp_ref = Rc::clone(¤t_composite);
|
|
dialog.choose(Some(&window_clone), gtk::gio::Cancellable::NONE, move |res| {
|
|
if let Ok(1) = res {
|
|
if let Some(c) = comp_ref.borrow().as_ref() {
|
|
c.borrow_mut().is_modified = false;
|
|
}
|
|
window_clone2.close();
|
|
}
|
|
});
|
|
return glib::Propagation::Stop;
|
|
}
|
|
}
|
|
glib::Propagation::Proceed
|
|
});
|
|
}
|
|
|
|
build_picker(&stack, &window, breeze_path.clone(), Rc::clone(¤t_composite));
|
|
|
|
if let Some(Ok(path)) = initial_icon {
|
|
build_editor(&stack, &window, path, breeze_path, Rc::clone(¤t_composite));
|
|
}
|
|
|
|
window.present();
|
|
});
|
|
|
|
app.run()
|
|
}
|
|
|
|
fn resolve_icon_arg(arg: &str, breeze_path: &Path) -> anyhow::Result<PathBuf> {
|
|
let as_given = PathBuf::from(arg);
|
|
if as_given.is_absolute() {
|
|
anyhow::ensure!(
|
|
as_given.exists(),
|
|
"Icon not found at {}",
|
|
as_given.display()
|
|
);
|
|
return Ok(as_given);
|
|
}
|
|
if as_given.exists() {
|
|
return Ok(as_given.canonicalize()?);
|
|
}
|
|
let under_breeze = breeze_path.join(arg);
|
|
anyhow::ensure!(
|
|
under_breeze.exists(),
|
|
"Icon not found at {} or {}",
|
|
as_given.display(),
|
|
under_breeze.display()
|
|
);
|
|
Ok(under_breeze.canonicalize()?)
|
|
}
|
|
|
|
fn build_picker(stack: &Stack, window: &ApplicationWindow, breeze_path: PathBuf, current_composite: Rc<RefCell<Option<Rc<RefCell<CompositeIcon>>>>>) {
|
|
let stack_for_callback = stack.clone();
|
|
let window_for_callback = window.clone();
|
|
let breeze_for_editor = breeze_path.clone();
|
|
let comp_callback = Rc::clone(¤t_composite);
|
|
|
|
let widget = ui::picker::build_picker_page(breeze_path, move |icon_path| {
|
|
build_editor(&stack_for_callback, &window_for_callback, icon_path, breeze_for_editor.clone(), Rc::clone(&comp_callback));
|
|
});
|
|
stack.add_named(&widget, Some("picker"));
|
|
stack.set_visible_child_name("picker");
|
|
}
|
|
|
|
fn build_editor(stack: &Stack, window: &ApplicationWindow, icon_path: PathBuf, breeze_path: PathBuf, current_composite: Rc<RefCell<Option<Rc<RefCell<CompositeIcon>>>>>) {
|
|
let mut scheme = match ColorScheme::load(&icon_path) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!("Failed to open {}: {e:#}", icon_path.display());
|
|
return;
|
|
}
|
|
};
|
|
scheme.auto_save = false;
|
|
|
|
let composite = Rc::new(RefCell::new(CompositeIcon::new(scheme)));
|
|
*current_composite.borrow_mut() = Some(Rc::clone(&composite));
|
|
|
|
let editor_container = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
|
|
|
let rebuild_editor = {
|
|
let composite = Rc::clone(&composite);
|
|
let stack = stack.clone();
|
|
let window = window.clone();
|
|
let editor_container = editor_container.clone();
|
|
let current_comp = Rc::clone(¤t_composite);
|
|
|
|
Rc::new(move || {
|
|
while let Some(child) = editor_container.first_child() {
|
|
editor_container.remove(&child);
|
|
}
|
|
|
|
let stack_clone = stack.clone();
|
|
let stack_emblem = stack.clone();
|
|
let window_clone = window.clone();
|
|
let comp = Rc::clone(&composite);
|
|
let comp_for_back = Rc::clone(&comp);
|
|
let current_comp_for_back = Rc::clone(¤t_comp);
|
|
|
|
let page = ui::editor::build_editor_page(
|
|
Rc::clone(&composite),
|
|
move || {
|
|
if comp_for_back.borrow().is_modified {
|
|
let dialog = gtk::AlertDialog::builder()
|
|
.message("You have unsaved changes. Are you sure you want to discard them?")
|
|
.detail("All unsaved changes will be lost.")
|
|
.buttons(["Cancel", "Discard"])
|
|
.cancel_button(0)
|
|
.default_button(1)
|
|
.build();
|
|
|
|
let stack_clone = stack_clone.clone();
|
|
let comp_ref = Rc::clone(¤t_comp_for_back);
|
|
dialog.choose(Some(&window_clone), gtk::gio::Cancellable::NONE, move |res| {
|
|
if let Ok(1) = res {
|
|
*comp_ref.borrow_mut() = None;
|
|
stack_clone.set_visible_child_name("picker");
|
|
}
|
|
});
|
|
} else {
|
|
*current_comp_for_back.borrow_mut() = None;
|
|
stack_clone.set_visible_child_name("picker");
|
|
}
|
|
},
|
|
move || {
|
|
stack_emblem.set_visible_child_name("emblem_picker");
|
|
},
|
|
{
|
|
let window_clone = window.clone();
|
|
let comp = Rc::clone(&comp);
|
|
move || {
|
|
let filter = gtk::FileFilter::new();
|
|
filter.set_name(Some("SVG Images"));
|
|
filter.add_mime_type("image/svg+xml");
|
|
filter.add_pattern("*.svg");
|
|
|
|
let filters = gtk::gio::ListStore::new::<gtk::FileFilter>();
|
|
filters.append(&filter);
|
|
|
|
let dialog = gtk::FileDialog::builder()
|
|
.title("Save Composite Icon As...")
|
|
.filters(&filters)
|
|
.default_filter(&filter)
|
|
.build();
|
|
let c = Rc::clone(&comp);
|
|
dialog.save(Some(&window_clone), gtk::gio::Cancellable::NONE, move |res| {
|
|
if let Ok(file) = res {
|
|
if let Some(path) = file.path() {
|
|
let rendered = c.borrow().render();
|
|
if let Err(e) = std::fs::write(&path, rendered) {
|
|
eprintln!("Failed to save composite to {}: {e}", path.display());
|
|
} else {
|
|
c.borrow_mut().is_modified = false;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
);
|
|
editor_container.append(&page);
|
|
})
|
|
};
|
|
|
|
rebuild_editor();
|
|
|
|
if let Some(existing) = stack.child_by_name("editor") {
|
|
stack.remove(&existing);
|
|
}
|
|
stack.add_named(&editor_container, Some("editor"));
|
|
|
|
let rebuild_editor_clone = Rc::clone(&rebuild_editor);
|
|
let stack_emblem_callback = stack.clone();
|
|
let comp_for_picker = Rc::clone(&composite);
|
|
|
|
let emblem_picker = ui::picker::build_picker_page(breeze_path, move |emblem_path| {
|
|
if let Ok(mut emblem_scheme) = ColorScheme::load_with_namespace(&emblem_path, "Emblem") {
|
|
emblem_scheme.auto_save = false;
|
|
let mut c = comp_for_picker.borrow_mut();
|
|
c.set_emblem(emblem_scheme);
|
|
c.is_modified = true;
|
|
drop(c);
|
|
rebuild_editor_clone();
|
|
stack_emblem_callback.set_visible_child_name("editor");
|
|
} else {
|
|
eprintln!("Failed to load emblem: {}", emblem_path.display());
|
|
}
|
|
});
|
|
|
|
if let Some(existing) = stack.child_by_name("emblem_picker") {
|
|
stack.remove(&existing);
|
|
}
|
|
stack.add_named(&emblem_picker, Some("emblem_picker"));
|
|
|
|
stack.set_visible_child_name("editor");
|
|
}
|