bundle and render branding icons
This commit is contained in:
parent
bee1dd892a
commit
e36107a6cf
|
|
@ -1800,6 +1800,16 @@ dependencies = [
|
|||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
|
|
@ -2225,6 +2235,7 @@ dependencies = [
|
|||
name = "graphite-iced-frontend"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"graph-craft",
|
||||
"graphite-editor",
|
||||
"iced_graphics",
|
||||
|
|
@ -2237,12 +2248,16 @@ dependencies = [
|
|||
"rand",
|
||||
"raw-window-handle",
|
||||
"reqwest",
|
||||
"resvg",
|
||||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"usvg",
|
||||
"wgpu",
|
||||
"wgpu-executor",
|
||||
"window_clipboard",
|
||||
"winit",
|
||||
]
|
||||
|
||||
|
|
@ -2684,7 +2699,7 @@ dependencies = [
|
|||
"log",
|
||||
"rustc-hash 2.1.1",
|
||||
"softbuffer",
|
||||
"tiny-skia",
|
||||
"tiny-skia 0.11.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2877,11 +2892,21 @@ dependencies = [
|
|||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"color_quant",
|
||||
"gif",
|
||||
"gif 0.13.3",
|
||||
"num-traits",
|
||||
"png 0.17.16",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
"zune-core 0.4.12",
|
||||
"zune-jpeg 0.4.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4493,6 +4518,12 @@ version = "1.8.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.5"
|
||||
|
|
@ -4903,6 +4934,23 @@ dependencies = [
|
|||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resvg"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9be183ad6a216aa96f33e4c8033b0988b8b3ea6fd2359d19af5bac4643fd8e81"
|
||||
dependencies = [
|
||||
"gif 0.14.2",
|
||||
"image-webp",
|
||||
"log",
|
||||
"pico-args",
|
||||
"rgb",
|
||||
"svgtypes",
|
||||
"tiny-skia 0.12.0",
|
||||
"usvg",
|
||||
"zune-jpeg 0.5.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.17.2"
|
||||
|
|
@ -4929,6 +4977,15 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
|
|
@ -5207,7 +5264,7 @@ dependencies = [
|
|||
"log",
|
||||
"memmap2",
|
||||
"smithay-client-toolkit",
|
||||
"tiny-skia",
|
||||
"tiny-skia 0.11.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5993,6 +6050,21 @@ dependencies = [
|
|||
"tiny-skia-path 0.11.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"png 0.18.1",
|
||||
"tiny-skia-path 0.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia-path"
|
||||
version = "0.11.4"
|
||||
|
|
@ -8068,11 +8140,26 @@ version = "0.4.12"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
"zune-core 0.4.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
|
||||
dependencies = [
|
||||
"zune-core 0.5.1",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -27,11 +27,16 @@ wgpu = { workspace = true }
|
|||
raw-window-handle = "0.6"
|
||||
pollster = "0.4"
|
||||
|
||||
dirs = { workspace = true }
|
||||
image = { workspace = true }
|
||||
include_dir = { workspace = true }
|
||||
rand = { workspace = true, features = ["thread_rng"] }
|
||||
reqwest = { workspace = true }
|
||||
resvg = { workspace = true }
|
||||
rfd = { workspace = true }
|
||||
usvg = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
window_clipboard = "0.5"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
use raw_window_handle::{DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle, WindowHandle};
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub struct ClipboardHandle {
|
||||
inner: Mutex<window_clipboard::Clipboard>,
|
||||
}
|
||||
|
||||
impl ClipboardHandle {
|
||||
pub fn new_from_raw(window_handle: RawWindowHandle, display_handle: RawDisplayHandle) -> Option<Self> {
|
||||
let provider = RawHandles { window: window_handle, display: display_handle };
|
||||
let connected = unsafe { window_clipboard::Clipboard::connect(&provider) };
|
||||
match connected {
|
||||
Ok(clipboard) => Some(Self { inner: Mutex::new(clipboard) }),
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to connect system clipboard: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_text(&self) -> Option<String> {
|
||||
let guard = match self.inner.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
tracing::warn!("clipboard mutex poisoned: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match guard.read() {
|
||||
Ok(text) => Some(text),
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to read clipboard: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_text(&self, text: &str) {
|
||||
let mut guard = match self.inner.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
tracing::warn!("clipboard mutex poisoned: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = guard.write(text.to_string()) {
|
||||
tracing::warn!("failed to write clipboard: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_selection(&self) -> Option<String> {
|
||||
let guard = match self.inner.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
tracing::warn!("clipboard mutex poisoned: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if let Some(result) = guard.read_primary() {
|
||||
match result {
|
||||
Ok(text) => Some(text),
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to read primary selection: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match guard.read() {
|
||||
Ok(text) => Some(text),
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to read clipboard fallback for selection: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_selection(&self, text: &str) {
|
||||
let mut guard = match self.inner.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
tracing::warn!("clipboard mutex poisoned: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(result) = guard.write_primary(text.to_string()) {
|
||||
if let Err(e) = result {
|
||||
tracing::warn!("failed to write primary selection: {e}");
|
||||
}
|
||||
} else if let Err(e) = guard.write(text.to_string()) {
|
||||
tracing::warn!("failed to write clipboard fallback for selection: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RawHandles {
|
||||
window: RawWindowHandle,
|
||||
display: RawDisplayHandle,
|
||||
}
|
||||
|
||||
impl HasDisplayHandle for RawHandles {
|
||||
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
||||
Ok(unsafe { DisplayHandle::borrow_raw(self.display) })
|
||||
}
|
||||
}
|
||||
|
||||
impl HasWindowHandle for RawHandles {
|
||||
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
||||
Ok(unsafe { WindowHandle::borrow_raw(self.window) })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
use graphite_editor::messages::prelude::DocumentId;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::thread;
|
||||
|
||||
const GRAPHITE_EXTENSION: &str = "graphite";
|
||||
const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "svg", "bmp", "gif"];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FileIoResult {
|
||||
Opened { path: PathBuf, content: Vec<u8> },
|
||||
Imported { path: PathBuf, content: Vec<u8> },
|
||||
SavedDocument { document_id: DocumentId, path: PathBuf },
|
||||
ExportComplete,
|
||||
Cancelled,
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
pub fn spawn_open_dialog(sender: Sender<FileIoResult>) {
|
||||
thread::spawn(move || {
|
||||
let mut all_exts: Vec<&str> = Vec::with_capacity(IMAGE_EXTENSIONS.len() + 1);
|
||||
all_exts.push(GRAPHITE_EXTENSION);
|
||||
all_exts.extend_from_slice(IMAGE_EXTENSIONS);
|
||||
|
||||
let dialog = rfd::FileDialog::new()
|
||||
.set_title("Open File")
|
||||
.add_filter("Graphite & Images", &all_exts)
|
||||
.add_filter("Graphite Document", &[GRAPHITE_EXTENSION])
|
||||
.add_filter("Images", IMAGE_EXTENSIONS);
|
||||
|
||||
let result = match dialog.pick_file() {
|
||||
Some(path) => match std::fs::read(&path) {
|
||||
Ok(content) => FileIoResult::Opened { path, content },
|
||||
Err(e) => FileIoResult::Failed(format!("Failed to read {}: {e}", path.display())),
|
||||
},
|
||||
None => FileIoResult::Cancelled,
|
||||
};
|
||||
let _ = sender.send(result);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn spawn_import_dialog(sender: Sender<FileIoResult>) {
|
||||
thread::spawn(move || {
|
||||
let dialog = rfd::FileDialog::new().set_title("Import Image").add_filter("Images", IMAGE_EXTENSIONS);
|
||||
|
||||
let result = match dialog.pick_file() {
|
||||
Some(path) => match std::fs::read(&path) {
|
||||
Ok(content) => FileIoResult::Imported { path, content },
|
||||
Err(e) => FileIoResult::Failed(format!("Failed to read {}: {e}", path.display())),
|
||||
},
|
||||
None => FileIoResult::Cancelled,
|
||||
};
|
||||
let _ = sender.send(result);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn spawn_save_dialog(sender: Sender<FileIoResult>, suggested_name: String, suggested_folder: Option<PathBuf>, content: Vec<u8>) {
|
||||
thread::spawn(move || {
|
||||
let result = match resolve_save_path(&suggested_name, suggested_folder.as_deref(), None) {
|
||||
Some(path) => match std::fs::write(&path, &content) {
|
||||
Ok(()) => FileIoResult::ExportComplete,
|
||||
Err(e) => FileIoResult::Failed(format!("Failed to write {}: {e}", path.display())),
|
||||
},
|
||||
None => FileIoResult::Cancelled,
|
||||
};
|
||||
let _ = sender.send(result);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn spawn_save_document(sender: Sender<FileIoResult>, document_id: DocumentId, suggested_name: String, explicit_path: Option<PathBuf>, suggested_folder: Option<PathBuf>, content: Vec<u8>) {
|
||||
thread::spawn(move || {
|
||||
let chosen = match explicit_path {
|
||||
Some(path) => Some(path),
|
||||
None => resolve_save_path(&suggested_name, suggested_folder.as_deref(), Some(GRAPHITE_EXTENSION)),
|
||||
};
|
||||
let result = match chosen {
|
||||
Some(path) => match std::fs::write(&path, &content) {
|
||||
Ok(()) => FileIoResult::SavedDocument { document_id, path },
|
||||
Err(e) => FileIoResult::Failed(format!("Failed to write {}: {e}", path.display())),
|
||||
},
|
||||
None => FileIoResult::Cancelled,
|
||||
};
|
||||
let _ = sender.send(result);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn spawn_export_image(sender: Sender<FileIoResult>, svg: String, name: String, mime: String, size: (f64, f64)) {
|
||||
thread::spawn(move || {
|
||||
let extension = match mime.as_str() {
|
||||
"image/svg+xml" => "svg",
|
||||
"image/png" => "png",
|
||||
"image/jpeg" => "jpg",
|
||||
_ => "bin",
|
||||
};
|
||||
let chosen = resolve_save_path(&name, None, Some(extension));
|
||||
let Some(path) = chosen else {
|
||||
let _ = sender.send(FileIoResult::Cancelled);
|
||||
return;
|
||||
};
|
||||
|
||||
let bytes = match mime.as_str() {
|
||||
"image/svg+xml" => Ok(svg.into_bytes()),
|
||||
"image/png" => rasterise_svg(&svg, size, false),
|
||||
"image/jpeg" => rasterise_svg(&svg, size, true),
|
||||
other => Err(format!("Unsupported export mime: {other}")),
|
||||
};
|
||||
|
||||
let result = match bytes.and_then(|b| std::fs::write(&path, &b).map_err(|e| format!("Failed to write {}: {e}", path.display()))) {
|
||||
Ok(()) => FileIoResult::ExportComplete,
|
||||
Err(message) => FileIoResult::Failed(message),
|
||||
};
|
||||
let _ = sender.send(result);
|
||||
});
|
||||
}
|
||||
|
||||
fn resolve_save_path(suggested_name: &str, suggested_folder: Option<&std::path::Path>, force_extension: Option<&str>) -> Option<PathBuf> {
|
||||
let mut dialog = rfd::FileDialog::new().set_title("Save File").set_file_name(suggested_name);
|
||||
if let Some(folder) = suggested_folder {
|
||||
dialog = dialog.set_directory(folder);
|
||||
}
|
||||
if let Some(ext) = force_extension {
|
||||
let label = format!("{} file", ext.to_ascii_uppercase());
|
||||
dialog = dialog.add_filter(label.as_str(), &[ext]);
|
||||
}
|
||||
let mut path = dialog.save_file()?;
|
||||
if let Some(ext) = force_extension {
|
||||
if path.extension().and_then(|e| e.to_str()).map(|e| !e.eq_ignore_ascii_case(ext)).unwrap_or(true) {
|
||||
path.set_extension(ext);
|
||||
}
|
||||
}
|
||||
Some(path)
|
||||
}
|
||||
|
||||
fn rasterise_svg(svg: &str, size: (f64, f64), jpeg: bool) -> Result<Vec<u8>, String> {
|
||||
let (width, height) = (size.0.max(1.0) as u32, size.1.max(1.0) as u32);
|
||||
let opts = usvg::Options::default();
|
||||
let tree = usvg::Tree::from_str(svg, &opts).map_err(|e| format!("SVG parse error: {e}"))?;
|
||||
|
||||
let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height).ok_or_else(|| "Pixmap allocation failed (zero or oversized dimensions)".to_string())?;
|
||||
if jpeg {
|
||||
pixmap.fill(resvg::tiny_skia::Color::WHITE);
|
||||
}
|
||||
|
||||
let tree_size = tree.size();
|
||||
let scale_x = width as f32 / tree_size.width();
|
||||
let scale_y = height as f32 / tree_size.height();
|
||||
let transform = resvg::tiny_skia::Transform::from_scale(scale_x, scale_y);
|
||||
resvg::render(&tree, transform, &mut pixmap.as_mut());
|
||||
|
||||
if jpeg {
|
||||
let rgba = pixmap.data();
|
||||
let buffer = image::RgbaImage::from_raw(width, height, rgba.to_vec()).ok_or_else(|| "RGBA buffer mismatch".to_string())?;
|
||||
let rgb = image::DynamicImage::ImageRgba8(buffer).to_rgb8();
|
||||
let mut out = Vec::with_capacity((width * height * 3) as usize);
|
||||
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, 92);
|
||||
encoder.encode(rgb.as_raw(), width, height, image::ExtendedColorType::Rgb8).map_err(|e| format!("JPEG encode error: {e}"))?;
|
||||
Ok(out)
|
||||
} else {
|
||||
pixmap.encode_png().map_err(|e| format!("PNG encode error: {e}"))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
use iced_widget::image::Handle;
|
||||
use include_dir::{Dir, include_dir};
|
||||
use resvg::tiny_skia;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
static BRANDING: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/../../branding/assets");
|
||||
|
||||
const RASTER_SCALE: f32 = 2.0;
|
||||
|
||||
static CACHE: LazyLock<Mutex<HashMap<&'static str, Handle>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
pub fn icon_handle(name: &str) -> Option<Handle> {
|
||||
let (key, path, size) = lookup(name)?;
|
||||
|
||||
if let Some(cached) = CACHE.lock().ok().and_then(|c| c.get(key).cloned()) {
|
||||
return Some(cached);
|
||||
}
|
||||
|
||||
let bytes = BRANDING.get_file(path)?.contents();
|
||||
let handle = if path.ends_with(".svg") {
|
||||
rasterize_svg(bytes, size)?
|
||||
} else {
|
||||
decode_raster(bytes)?
|
||||
};
|
||||
|
||||
if let Ok(mut cache) = CACHE.lock() {
|
||||
cache.insert(key, handle.clone());
|
||||
}
|
||||
Some(handle)
|
||||
}
|
||||
|
||||
fn rasterize_svg(bytes: &[u8], size: Option<u32>) -> Option<Handle> {
|
||||
let opt = usvg::Options::default();
|
||||
let tree = usvg::Tree::from_data(bytes, &opt).ok()?;
|
||||
let svg_size = tree.size();
|
||||
|
||||
let (width, height) = match size {
|
||||
Some(s) => {
|
||||
let s_f = s as f32 * RASTER_SCALE;
|
||||
(s_f.max(1.0).round() as u32, s_f.max(1.0).round() as u32)
|
||||
}
|
||||
None => {
|
||||
let w = (svg_size.width() * RASTER_SCALE).max(1.0).round() as u32;
|
||||
let h = (svg_size.height() * RASTER_SCALE).max(1.0).round() as u32;
|
||||
(w, h)
|
||||
}
|
||||
};
|
||||
|
||||
let scale_x = width as f32 / svg_size.width();
|
||||
let scale_y = height as f32 / svg_size.height();
|
||||
let transform = tiny_skia::Transform::from_scale(scale_x, scale_y);
|
||||
|
||||
let mut pixmap = tiny_skia::Pixmap::new(width, height)?;
|
||||
resvg::render(&tree, transform, &mut pixmap.as_mut());
|
||||
|
||||
let rgba = pixmap.take_demultiplied();
|
||||
Some(Handle::from_rgba(width, height, rgba))
|
||||
}
|
||||
|
||||
fn decode_raster(bytes: &[u8]) -> Option<Handle> {
|
||||
let img = image::load_from_memory(bytes).ok()?.to_rgba8();
|
||||
let (w, h) = img.dimensions();
|
||||
Some(Handle::from_rgba(w, h, img.into_raw()))
|
||||
}
|
||||
|
||||
fn lookup(name: &str) -> Option<(&'static str, &'static str, Option<u32>)> {
|
||||
for &(n, path, size) in ICONS {
|
||||
if n == name {
|
||||
return Some((n, path, size));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
const ICONS: &[(&str, &str, Option<u32>)] = &[
|
||||
("GraphiteLogotypeSolid", "graphics/graphite-logotype-solid.svg", None),
|
||||
("Add", "icon-12px-solid/add.svg", Some(12)),
|
||||
("Checkmark", "icon-12px-solid/checkmark.svg", Some(12)),
|
||||
("Clipped", "icon-12px-solid/clipped.svg", Some(12)),
|
||||
("CloseX", "icon-12px-solid/close-x.svg", Some(12)),
|
||||
("Delay", "icon-12px-solid/delay.svg", Some(12)),
|
||||
("Dot", "icon-12px-solid/dot.svg", Some(12)),
|
||||
("DotThick", "icon-12px-solid/dot-thick.svg", Some(12)),
|
||||
("DropdownArrow", "icon-12px-solid/dropdown-arrow.svg", Some(12)),
|
||||
("Edit12px", "icon-12px-solid/edit-12px.svg", Some(12)),
|
||||
("Empty12px", "icon-12px-solid/empty-12px.svg", Some(12)),
|
||||
("Failure", "icon-12px-solid/failure.svg", Some(12)),
|
||||
("FullscreenEnter", "icon-12px-solid/fullscreen-enter.svg", Some(12)),
|
||||
("FullscreenExit", "icon-12px-solid/fullscreen-exit.svg", Some(12)),
|
||||
("Grid", "icon-12px-solid/grid.svg", Some(12)),
|
||||
("GridDotted", "icon-12px-solid/grid-dotted.svg", Some(12)),
|
||||
("Info", "icon-12px-solid/info.svg", Some(12)),
|
||||
("KeyboardArrowDown", "icon-12px-solid/keyboard-arrow-down.svg", Some(12)),
|
||||
("KeyboardArrowLeft", "icon-12px-solid/keyboard-arrow-left.svg", Some(12)),
|
||||
("KeyboardArrowRight", "icon-12px-solid/keyboard-arrow-right.svg", Some(12)),
|
||||
("KeyboardArrowUp", "icon-12px-solid/keyboard-arrow-up.svg", Some(12)),
|
||||
("KeyboardBackspace", "icon-12px-solid/keyboard-backspace.svg", Some(12)),
|
||||
("KeyboardCommand", "icon-12px-solid/keyboard-command.svg", Some(12)),
|
||||
("KeyboardControl", "icon-12px-solid/keyboard-control.svg", Some(12)),
|
||||
("KeyboardEnter", "icon-12px-solid/keyboard-enter.svg", Some(12)),
|
||||
("KeyboardOption", "icon-12px-solid/keyboard-option.svg", Some(12)),
|
||||
("KeyboardShift", "icon-12px-solid/keyboard-shift.svg", Some(12)),
|
||||
("KeyboardSpace", "icon-12px-solid/keyboard-space.svg", Some(12)),
|
||||
("KeyboardTab", "icon-12px-solid/keyboard-tab.svg", Some(12)),
|
||||
("License12px", "icon-12px-solid/license-12px.svg", Some(12)),
|
||||
("Link", "icon-12px-solid/link.svg", Some(12)),
|
||||
("Overlays", "icon-12px-solid/overlays.svg", Some(12)),
|
||||
("Remove", "icon-12px-solid/remove.svg", Some(12)),
|
||||
("RenderModeNormal", "icon-12px-solid/render-mode-normal.svg", Some(12)),
|
||||
("RenderModeOutline", "icon-12px-solid/render-mode-outline.svg", Some(12)),
|
||||
("RenderModePixels", "icon-12px-solid/render-mode-pixels.svg", Some(12)),
|
||||
("RenderModeSvg", "icon-12px-solid/render-mode-svg.svg", Some(12)),
|
||||
("Snapping", "icon-12px-solid/snapping.svg", Some(12)),
|
||||
("SwapHorizontal", "icon-12px-solid/swap-horizontal.svg", Some(12)),
|
||||
("SwapVertical", "icon-12px-solid/swap-vertical.svg", Some(12)),
|
||||
("VerticalEllipsis", "icon-12px-solid/vertical-ellipsis.svg", Some(12)),
|
||||
("Warning", "icon-12px-solid/warning.svg", Some(12)),
|
||||
("WindowButtonWinClose", "icon-12px-solid/window-button-win-close.svg", Some(12)),
|
||||
("WindowButtonWinMaximize", "icon-12px-solid/window-button-win-maximize.svg", Some(12)),
|
||||
("WindowButtonWinMinimize", "icon-12px-solid/window-button-win-minimize.svg", Some(12)),
|
||||
("WindowButtonWinRestoreDown", "icon-12px-solid/window-button-win-restore-down.svg", Some(12)),
|
||||
("WorkingColors", "icon-12px-solid/working-colors.svg", Some(12)),
|
||||
("AlignBottom", "icon-16px-solid/align-bottom.svg", Some(16)),
|
||||
("AlignHorizontalCenter", "icon-16px-solid/align-horizontal-center.svg", Some(16)),
|
||||
("AlignLeft", "icon-16px-solid/align-left.svg", Some(16)),
|
||||
("AlignRight", "icon-16px-solid/align-right.svg", Some(16)),
|
||||
("AlignTop", "icon-16px-solid/align-top.svg", Some(16)),
|
||||
("AlignVerticalCenter", "icon-16px-solid/align-vertical-center.svg", Some(16)),
|
||||
("Artboard", "icon-16px-solid/artboard.svg", Some(16)),
|
||||
("BooleanDifference", "icon-16px-solid/boolean-difference.svg", Some(16)),
|
||||
("BooleanDivide", "icon-16px-solid/boolean-divide.svg", Some(16)),
|
||||
("BooleanIntersect", "icon-16px-solid/boolean-intersect.svg", Some(16)),
|
||||
("BooleanSubtractBack", "icon-16px-solid/boolean-subtract-back.svg", Some(16)),
|
||||
("BooleanSubtractFront", "icon-16px-solid/boolean-subtract-front.svg", Some(16)),
|
||||
("BooleanUnion", "icon-16px-solid/boolean-union.svg", Some(16)),
|
||||
("Bug", "icon-16px-solid/bug.svg", Some(16)),
|
||||
("CheckboxChecked", "icon-16px-solid/checkbox-checked.svg", Some(16)),
|
||||
("CheckboxUnchecked", "icon-16px-solid/checkbox-unchecked.svg", Some(16)),
|
||||
("Close", "icon-16px-solid/close.svg", Some(16)),
|
||||
("CloseAll", "icon-16px-solid/close-all.svg", Some(16)),
|
||||
("Code", "icon-16px-solid/code.svg", Some(16)),
|
||||
("Copy", "icon-16px-solid/copy.svg", Some(16)),
|
||||
("Credits", "icon-16px-solid/credits.svg", Some(16)),
|
||||
("CustomColor", "icon-16px-solid/custom-color.svg", Some(16)),
|
||||
("Cut", "icon-16px-solid/cut.svg", Some(16)),
|
||||
("DeselectAll", "icon-16px-solid/deselect-all.svg", Some(16)),
|
||||
("Edit", "icon-16px-solid/edit.svg", Some(16)),
|
||||
("Empty", "icon-16px-solid/empty.svg", Some(16)),
|
||||
("ExpandFillStroke", "icon-16px-solid/expand-fill-stroke.svg", Some(16)),
|
||||
("Eyedropper", "icon-16px-solid/eyedropper.svg", Some(16)),
|
||||
("EyeHidden", "icon-16px-solid/eye-hidden.svg", Some(16)),
|
||||
("EyeHide", "icon-16px-solid/eye-hide.svg", Some(16)),
|
||||
("EyeShow", "icon-16px-solid/eye-show.svg", Some(16)),
|
||||
("EyeVisible", "icon-16px-solid/eye-visible.svg", Some(16)),
|
||||
("File", "icon-16px-solid/file.svg", Some(16)),
|
||||
("FileExport", "icon-16px-solid/file-export.svg", Some(16)),
|
||||
("FileImport", "icon-16px-solid/file-import.svg", Some(16)),
|
||||
("FlipHorizontal", "icon-16px-solid/flip-horizontal.svg", Some(16)),
|
||||
("FlipVertical", "icon-16px-solid/flip-vertical.svg", Some(16)),
|
||||
("Folder", "icon-16px-solid/folder.svg", Some(16)),
|
||||
("FolderOpen", "icon-16px-solid/folder-open.svg", Some(16)),
|
||||
("FrameAll", "icon-16px-solid/frame-all.svg", Some(16)),
|
||||
("FrameSelected", "icon-16px-solid/frame-selected.svg", Some(16)),
|
||||
("GraphiteLogo", "icon-16px-solid/graphite-logo.svg", Some(16)),
|
||||
("GraphViewClosed", "icon-16px-solid/graph-view-closed.svg", Some(16)),
|
||||
("GraphViewOpen", "icon-16px-solid/graph-view-open.svg", Some(16)),
|
||||
("HandleVisibilityAll", "icon-16px-solid/handle-visibility-all.svg", Some(16)),
|
||||
("HandleVisibilityFrontier", "icon-16px-solid/handle-visibility-frontier.svg", Some(16)),
|
||||
("HandleVisibilitySelected", "icon-16px-solid/handle-visibility-selected.svg", Some(16)),
|
||||
("Heart", "icon-16px-solid/heart.svg", Some(16)),
|
||||
("HistoryRedo", "icon-16px-solid/history-redo.svg", Some(16)),
|
||||
("HistoryUndo", "icon-16px-solid/history-undo.svg", Some(16)),
|
||||
("IconsGrid", "icon-16px-solid/icons-grid.svg", Some(16)),
|
||||
("Image", "icon-16px-solid/image.svg", Some(16)),
|
||||
("InterpolationBlend", "icon-16px-solid/interpolation-blend.svg", Some(16)),
|
||||
("InterpolationMorph", "icon-16px-solid/interpolation-morph.svg", Some(16)),
|
||||
("Layer", "icon-16px-solid/layer.svg", Some(16)),
|
||||
("License", "icon-16px-solid/license.svg", Some(16)),
|
||||
("NewLayer", "icon-16px-solid/new-layer.svg", Some(16)),
|
||||
("Node", "icon-16px-solid/node.svg", Some(16)),
|
||||
("NodeBlur", "icon-16px-solid/node-blur.svg", Some(16)),
|
||||
("NodeBrushwork", "icon-16px-solid/node-brushwork.svg", Some(16)),
|
||||
("NodeColorCorrection", "icon-16px-solid/node-color-correction.svg", Some(16)),
|
||||
("NodeGradient", "icon-16px-solid/node-gradient.svg", Some(16)),
|
||||
("NodeMagicWand", "icon-16px-solid/node-magic-wand.svg", Some(16)),
|
||||
("NodeMask", "icon-16px-solid/node-mask.svg", Some(16)),
|
||||
("NodeMotionBlur", "icon-16px-solid/node-motion-blur.svg", Some(16)),
|
||||
("NodeNodes", "icon-16px-solid/node-nodes.svg", Some(16)),
|
||||
("NodeOutput", "icon-16px-solid/node-output.svg", Some(16)),
|
||||
("NodeShape", "icon-16px-solid/node-shape.svg", Some(16)),
|
||||
("NodeText", "icon-16px-solid/node-text.svg", Some(16)),
|
||||
("NodeTransform", "icon-16px-solid/node-transform.svg", Some(16)),
|
||||
("PadlockLocked", "icon-16px-solid/padlock-locked.svg", Some(16)),
|
||||
("PadlockUnlocked", "icon-16px-solid/padlock-unlocked.svg", Some(16)),
|
||||
("Paste", "icon-16px-solid/paste.svg", Some(16)),
|
||||
("PinActive", "icon-16px-solid/pin-active.svg", Some(16)),
|
||||
("PinInactive", "icon-16px-solid/pin-inactive.svg", Some(16)),
|
||||
("PlaybackPause", "icon-16px-solid/playback-pause.svg", Some(16)),
|
||||
("PlaybackPlay", "icon-16px-solid/playback-play.svg", Some(16)),
|
||||
("PlaybackToEnd", "icon-16px-solid/playback-to-end.svg", Some(16)),
|
||||
("PlaybackToStart", "icon-16px-solid/playback-to-start.svg", Some(16)),
|
||||
("Random", "icon-16px-solid/random.svg", Some(16)),
|
||||
("Reload", "icon-16px-solid/reload.svg", Some(16)),
|
||||
("Reset", "icon-16px-solid/reset.svg", Some(16)),
|
||||
("Resync", "icon-16px-solid/resync.svg", Some(16)),
|
||||
("Reverse", "icon-16px-solid/reverse.svg", Some(16)),
|
||||
("ReverseRadialGradientToLeft", "icon-16px-solid/reverse-radial-gradient-to-left.svg", Some(16)),
|
||||
("ReverseRadialGradientToRight", "icon-16px-solid/reverse-radial-gradient-to-right.svg", Some(16)),
|
||||
("Save", "icon-16px-solid/save.svg", Some(16)),
|
||||
("SelectAll", "icon-16px-solid/select-all.svg", Some(16)),
|
||||
("SelectParent", "icon-16px-solid/select-parent.svg", Some(16)),
|
||||
("Settings", "icon-16px-solid/settings.svg", Some(16)),
|
||||
("SmallDot", "icon-16px-solid/small-dot.svg", Some(16)),
|
||||
("Stack", "icon-16px-solid/stack.svg", Some(16)),
|
||||
("StackBottom", "icon-16px-solid/stack-bottom.svg", Some(16)),
|
||||
("StackHollow", "icon-16px-solid/stack-hollow.svg", Some(16)),
|
||||
("StackLower", "icon-16px-solid/stack-lower.svg", Some(16)),
|
||||
("StackRaise", "icon-16px-solid/stack-raise.svg", Some(16)),
|
||||
("StackReverse", "icon-16px-solid/stack-reverse.svg", Some(16)),
|
||||
("StrokeAlignCenter", "icon-16px-solid/stroke-align-center.svg", Some(16)),
|
||||
("StrokeAlignInside", "icon-16px-solid/stroke-align-inside.svg", Some(16)),
|
||||
("StrokeAlignOutside", "icon-16px-solid/stroke-align-outside.svg", Some(16)),
|
||||
("StrokeCapButt", "icon-16px-solid/stroke-cap-butt.svg", Some(16)),
|
||||
("StrokeCapRound", "icon-16px-solid/stroke-cap-round.svg", Some(16)),
|
||||
("StrokeCapSquare", "icon-16px-solid/stroke-cap-square.svg", Some(16)),
|
||||
("StrokeJoinBevel", "icon-16px-solid/stroke-join-bevel.svg", Some(16)),
|
||||
("StrokeJoinMiter", "icon-16px-solid/stroke-join-miter.svg", Some(16)),
|
||||
("StrokeJoinRound", "icon-16px-solid/stroke-join-round.svg", Some(16)),
|
||||
("StrokeOrderAbove", "icon-16px-solid/stroke-order-above.svg", Some(16)),
|
||||
("StrokeOrderBelow", "icon-16px-solid/stroke-order-below.svg", Some(16)),
|
||||
("TextAlignCenter", "icon-16px-solid/text-align-center.svg", Some(16)),
|
||||
("TextAlignLeft", "icon-16px-solid/text-align-left.svg", Some(16)),
|
||||
("TextAlignRight", "icon-16px-solid/text-align-right.svg", Some(16)),
|
||||
("TextAlignSpineAway", "icon-16px-solid/text-align-spine-away.svg", Some(16)),
|
||||
("TextAlignSpineTowards", "icon-16px-solid/text-align-spine-towards.svg", Some(16)),
|
||||
("TextJustifyAll", "icon-16px-solid/text-justify-all.svg", Some(16)),
|
||||
("TextJustifyCenter", "icon-16px-solid/text-justify-center.svg", Some(16)),
|
||||
("TextJustifyLeft", "icon-16px-solid/text-justify-left.svg", Some(16)),
|
||||
("TextJustifyRight", "icon-16px-solid/text-justify-right.svg", Some(16)),
|
||||
("Tilt", "icon-16px-solid/tilt.svg", Some(16)),
|
||||
("TiltReset", "icon-16px-solid/tilt-reset.svg", Some(16)),
|
||||
("TransformationGrab", "icon-16px-solid/transformation-grab.svg", Some(16)),
|
||||
("TransformationRotate", "icon-16px-solid/transformation-rotate.svg", Some(16)),
|
||||
("TransformationScale", "icon-16px-solid/transformation-scale.svg", Some(16)),
|
||||
("Trash", "icon-16px-solid/trash.svg", Some(16)),
|
||||
("TurnNegative90", "icon-16px-solid/turn-negative-90.svg", Some(16)),
|
||||
("TurnPositive90", "icon-16px-solid/turn-positive-90.svg", Some(16)),
|
||||
("UserManual", "icon-16px-solid/user-manual.svg", Some(16)),
|
||||
("ViewportDesignMode", "icon-16px-solid/viewport-design-mode.svg", Some(16)),
|
||||
("ViewportGuideMode", "icon-16px-solid/viewport-guide-mode.svg", Some(16)),
|
||||
("ViewportSelectMode", "icon-16px-solid/viewport-select-mode.svg", Some(16)),
|
||||
("Volunteer", "icon-16px-solid/volunteer.svg", Some(16)),
|
||||
("Website", "icon-16px-solid/website.svg", Some(16)),
|
||||
("WorkingColorsPrimary", "icon-16px-solid/working-colors-primary.svg", Some(16)),
|
||||
("WorkingColorsSecondary", "icon-16px-solid/working-colors-secondary.svg", Some(16)),
|
||||
("Zoom1x", "icon-16px-solid/zoom-1x.svg", Some(16)),
|
||||
("Zoom2x", "icon-16px-solid/zoom-2x.svg", Some(16)),
|
||||
("ZoomIn", "icon-16px-solid/zoom-in.svg", Some(16)),
|
||||
("ZoomOut", "icon-16px-solid/zoom-out.svg", Some(16)),
|
||||
("ZoomReset", "icon-16px-solid/zoom-reset.svg", Some(16)),
|
||||
("MouseHintDrag", "icon-16px-two-tone/mouse-hint-drag.svg", Some(16)),
|
||||
("MouseHintLmb", "icon-16px-two-tone/mouse-hint-lmb.svg", Some(16)),
|
||||
("MouseHintLmbDouble", "icon-16px-two-tone/mouse-hint-lmb-double.svg", Some(16)),
|
||||
("MouseHintLmbDrag", "icon-16px-two-tone/mouse-hint-lmb-drag.svg", Some(16)),
|
||||
("MouseHintMmb", "icon-16px-two-tone/mouse-hint-mmb.svg", Some(16)),
|
||||
("MouseHintMmbDrag", "icon-16px-two-tone/mouse-hint-mmb-drag.svg", Some(16)),
|
||||
("MouseHintNone", "icon-16px-two-tone/mouse-hint-none.svg", Some(16)),
|
||||
("MouseHintRmb", "icon-16px-two-tone/mouse-hint-rmb.svg", Some(16)),
|
||||
("MouseHintRmbDouble", "icon-16px-two-tone/mouse-hint-rmb-double.svg", Some(16)),
|
||||
("MouseHintRmbDrag", "icon-16px-two-tone/mouse-hint-rmb-drag.svg", Some(16)),
|
||||
("MouseHintScrollDown", "icon-16px-two-tone/mouse-hint-scroll-down.svg", Some(16)),
|
||||
("MouseHintScrollUp", "icon-16px-two-tone/mouse-hint-scroll-up.svg", Some(16)),
|
||||
("GeneralArtboardTool", "icon-24px-two-tone/general-artboard-tool.svg", Some(24)),
|
||||
("GeneralEyedropperTool", "icon-24px-two-tone/general-eyedropper-tool.svg", Some(24)),
|
||||
("GeneralFillTool", "icon-24px-two-tone/general-fill-tool.svg", Some(24)),
|
||||
("GeneralGradientTool", "icon-24px-two-tone/general-gradient-tool.svg", Some(24)),
|
||||
("GeneralNavigateTool", "icon-24px-two-tone/general-navigate-tool.svg", Some(24)),
|
||||
("GeneralSelectTool", "icon-24px-two-tone/general-select-tool.svg", Some(24)),
|
||||
("RasterBrushTool", "icon-24px-two-tone/raster-brush-tool.svg", Some(24)),
|
||||
("RasterCloneTool", "icon-24px-two-tone/raster-clone-tool.svg", Some(24)),
|
||||
("RasterDetailTool", "icon-24px-two-tone/raster-detail-tool.svg", Some(24)),
|
||||
("RasterHealTool", "icon-24px-two-tone/raster-heal-tool.svg", Some(24)),
|
||||
("RasterPatchTool", "icon-24px-two-tone/raster-patch-tool.svg", Some(24)),
|
||||
("RasterRelightTool", "icon-24px-two-tone/raster-relight-tool.svg", Some(24)),
|
||||
("VectorEllipseTool", "icon-24px-two-tone/vector-ellipse-tool.svg", Some(24)),
|
||||
("VectorFreehandTool", "icon-24px-two-tone/vector-freehand-tool.svg", Some(24)),
|
||||
("VectorLineTool", "icon-24px-two-tone/vector-line-tool.svg", Some(24)),
|
||||
("VectorPathTool", "icon-24px-two-tone/vector-path-tool.svg", Some(24)),
|
||||
("VectorPenTool", "icon-24px-two-tone/vector-pen-tool.svg", Some(24)),
|
||||
("VectorPolygonTool", "icon-24px-two-tone/vector-polygon-tool.svg", Some(24)),
|
||||
("VectorRectangleTool", "icon-24px-two-tone/vector-rectangle-tool.svg", Some(24)),
|
||||
("VectorSplineTool", "icon-24px-two-tone/vector-spline-tool.svg", Some(24)),
|
||||
("VectorTextTool", "icon-24px-two-tone/vector-text-tool.svg", Some(24)),
|
||||
];
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
mod app;
|
||||
mod clipboard;
|
||||
mod file_io;
|
||||
mod icons;
|
||||
mod input;
|
||||
mod layout;
|
||||
mod persist;
|
||||
mod pointer;
|
||||
mod shell;
|
||||
mod window_control;
|
||||
|
||||
mod viewport;
|
||||
mod viewport_widget;
|
||||
mod widgets;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
use graphite_editor::messages::frontend::utility_types::PersistedState;
|
||||
use graphite_editor::messages::prelude::DocumentId;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const APP_DIRECTORY_NAME: &str = if cfg!(target_os = "linux") { "graphite" } else { "Graphite" };
|
||||
const STATE_FILE_NAME: &str = "state.json";
|
||||
const PREFERENCES_FILE_NAME: &str = "preferences.json";
|
||||
const DOCUMENTS_DIRECTORY_NAME: &str = "documents";
|
||||
const DOCUMENT_FILE_EXTENSION: &str = "graphite";
|
||||
|
||||
fn root_dir() -> Option<PathBuf> {
|
||||
let base = dirs::config_local_dir().or_else(dirs::data_local_dir)?;
|
||||
let path = base.join(APP_DIRECTORY_NAME);
|
||||
if let Err(e) = std::fs::create_dir_all(&path) {
|
||||
tracing::warn!("failed to create graphite config directory at {path:?}: {e}");
|
||||
return None;
|
||||
}
|
||||
Some(path)
|
||||
}
|
||||
|
||||
fn documents_dir() -> Option<PathBuf> {
|
||||
let path = root_dir()?.join(DOCUMENTS_DIRECTORY_NAME);
|
||||
if let Err(e) = std::fs::create_dir_all(&path) {
|
||||
tracing::warn!("failed to create documents directory at {path:?}: {e}");
|
||||
return None;
|
||||
}
|
||||
Some(path)
|
||||
}
|
||||
|
||||
fn state_path() -> Option<PathBuf> {
|
||||
Some(root_dir()?.join(STATE_FILE_NAME))
|
||||
}
|
||||
|
||||
fn preferences_path() -> Option<PathBuf> {
|
||||
Some(root_dir()?.join(PREFERENCES_FILE_NAME))
|
||||
}
|
||||
|
||||
fn document_path(id: DocumentId) -> Option<PathBuf> {
|
||||
Some(documents_dir()?.join(format!("{:x}.{}", id.0, DOCUMENT_FILE_EXTENSION)))
|
||||
}
|
||||
|
||||
pub fn read_state() -> Option<PersistedState> {
|
||||
let path = state_path()?;
|
||||
let raw = match std::fs::read_to_string(&path) {
|
||||
Ok(raw) => raw,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None,
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to read persisted state from {path:?}: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match serde_json::from_str::<PersistedState>(&raw) {
|
||||
Ok(state) => Some(state),
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to parse persisted state at {path:?}: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_state(state: &PersistedState) {
|
||||
let Some(path) = state_path() else { return };
|
||||
let serialized = match serde_json::to_string_pretty(state) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to serialize persisted state: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = std::fs::write(&path, serialized) {
|
||||
tracing::warn!("failed to write persisted state to {path:?}: {e}");
|
||||
return;
|
||||
}
|
||||
garbage_collect_documents(state);
|
||||
}
|
||||
|
||||
pub fn read_document(document_id: DocumentId) -> Option<String> {
|
||||
let path = document_path(document_id)?;
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => Some(content),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to read document {document_id:?} from {path:?}: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_document(document_id: DocumentId, content: &str) {
|
||||
let Some(path) = document_path(document_id) else { return };
|
||||
if let Err(e) = std::fs::write(&path, content) {
|
||||
tracing::warn!("failed to write document {document_id:?} to {path:?}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_document(document_id: DocumentId) {
|
||||
let Some(path) = document_path(document_id) else { return };
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => tracing::warn!("failed to delete document {document_id:?} at {path:?}: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_preferences() -> Option<String> {
|
||||
let path = preferences_path()?;
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(raw) => Some(raw),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to read preferences from {path:?}: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_preferences(json: &str) {
|
||||
let Some(path) = preferences_path() else { return };
|
||||
if let Err(e) = std::fs::write(&path, json) {
|
||||
tracing::warn!("failed to write preferences to {path:?}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
fn garbage_collect_documents(state: &PersistedState) {
|
||||
let Some(dir) = documents_dir() else { return };
|
||||
let valid: std::collections::HashSet<PathBuf> = state.documents.iter().filter_map(|doc| document_path(doc.id)).collect();
|
||||
let entries = match std::fs::read_dir(&dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to scan documents directory at {dir:?}: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_file() && !valid.contains(&path) {
|
||||
if let Err(e) = std::fs::remove_file(&path) {
|
||||
tracing::warn!("failed to remove orphaned document at {path:?}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
use graphite_editor::consts::DOUBLE_CLICK_MILLISECONDS;
|
||||
use graphite_editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
|
||||
use graphite_editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta};
|
||||
|
||||
use winit::event::{ElementState, MouseButton, MouseScrollDelta};
|
||||
|
||||
pub const DEFAULT_TOOLSHELF_WIDTH: f64 = 48.0;
|
||||
pub const DEFAULT_SIDEBAR_WIDTH: f64 = 340.0;
|
||||
pub const DEFAULT_MENUBAR_HEIGHT: f64 = 28.0;
|
||||
pub const DEFAULT_STATUSBAR_HEIGHT: f64 = 24.0;
|
||||
|
||||
const SCROLL_LINE_HEIGHT: f64 = 16.0;
|
||||
const SCROLL_LINE_WIDTH: f64 = 16.0;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ViewportRegion {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
}
|
||||
|
||||
impl ViewportRegion {
|
||||
pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
|
||||
Self { x, y, width, height }
|
||||
}
|
||||
|
||||
pub fn from_window(window_width: f64, window_height: f64) -> Self {
|
||||
let x = DEFAULT_TOOLSHELF_WIDTH;
|
||||
let y = DEFAULT_MENUBAR_HEIGHT;
|
||||
let width = (window_width - DEFAULT_TOOLSHELF_WIDTH - DEFAULT_SIDEBAR_WIDTH).max(0.0);
|
||||
let height = (window_height - DEFAULT_MENUBAR_HEIGHT - DEFAULT_STATUSBAR_HEIGHT).max(0.0);
|
||||
Self { x, y, width, height }
|
||||
}
|
||||
|
||||
pub fn contains(&self, position_logical: (f64, f64)) -> bool {
|
||||
let (px, py) = position_logical;
|
||||
px >= self.x && py >= self.y && px < self.x + self.width && py < self.y + self.height
|
||||
}
|
||||
|
||||
pub fn to_viewport_local(&self, position_logical: (f64, f64)) -> (f64, f64) {
|
||||
(position_logical.0 - self.x, position_logical.1 - self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ViewportRegion {
|
||||
fn default() -> Self {
|
||||
Self { x: DEFAULT_TOOLSHELF_WIDTH, y: DEFAULT_MENUBAR_HEIGHT, width: 0.0, height: 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PointerHit {
|
||||
Viewport,
|
||||
Chrome,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct EditorPointerState {
|
||||
buttons: u8,
|
||||
last_position_logical: (f64, f64),
|
||||
last_down: Option<LastDown>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct LastDown {
|
||||
button: MouseButton,
|
||||
at: Instant,
|
||||
position_logical: (f64, f64),
|
||||
}
|
||||
|
||||
impl EditorPointerState {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn buttons(&self) -> u8 {
|
||||
self.buttons
|
||||
}
|
||||
|
||||
pub fn mouse_keys(&self) -> MouseKeys {
|
||||
MouseKeys::from_bits_truncate(self.buttons)
|
||||
}
|
||||
|
||||
pub fn last_position_logical(&self) -> (f64, f64) {
|
||||
self.last_position_logical
|
||||
}
|
||||
|
||||
pub fn record_move(&mut self, position_logical: (f64, f64)) {
|
||||
self.last_position_logical = position_logical;
|
||||
}
|
||||
|
||||
pub fn record_button(&mut self, button: MouseButton, state: ElementState, position_logical: (f64, f64)) -> ButtonTransition {
|
||||
self.last_position_logical = position_logical;
|
||||
let bit = translate_winit_button_to_bit(button);
|
||||
match state {
|
||||
ElementState::Pressed => {
|
||||
self.buttons |= bit;
|
||||
let now = Instant::now();
|
||||
let double = self
|
||||
.last_down
|
||||
.is_some_and(|prev| prev.button == button && now.duration_since(prev.at) <= Duration::from_millis(DOUBLE_CLICK_MILLISECONDS));
|
||||
self.last_down = Some(LastDown { button, at: now, position_logical });
|
||||
if double {
|
||||
ButtonTransition::DownDouble
|
||||
} else {
|
||||
ButtonTransition::Down
|
||||
}
|
||||
}
|
||||
ElementState::Released => {
|
||||
self.buttons &= !bit;
|
||||
ButtonTransition::Up
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn classify(&self, position_logical: (f64, f64), region: ViewportRegion) -> PointerHit {
|
||||
if region.contains(position_logical) { PointerHit::Viewport } else { PointerHit::Chrome }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ButtonTransition {
|
||||
Down,
|
||||
DownDouble,
|
||||
Up,
|
||||
}
|
||||
|
||||
pub fn translate_winit_button_to_bit(button: MouseButton) -> u8 {
|
||||
match button {
|
||||
MouseButton::Left => MouseKeys::LEFT.bits(),
|
||||
MouseButton::Right => MouseKeys::RIGHT.bits(),
|
||||
MouseButton::Middle => MouseKeys::MIDDLE.bits(),
|
||||
MouseButton::Back => MouseKeys::BACK.bits(),
|
||||
MouseButton::Forward => MouseKeys::FORWARD.bits(),
|
||||
MouseButton::Other(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_editor_mouse_state(position_logical: (f64, f64), buttons: u8) -> EditorMouseState {
|
||||
EditorMouseState::from_keys_and_editor_position(buttons, position_logical.into())
|
||||
}
|
||||
|
||||
pub fn build_editor_mouse_state_with_scroll(position_logical: (f64, f64), buttons: u8, scroll: ScrollDelta) -> EditorMouseState {
|
||||
let mut state = build_editor_mouse_state(position_logical, buttons);
|
||||
state.scroll_delta = scroll;
|
||||
state
|
||||
}
|
||||
|
||||
pub fn winit_scroll_to_delta(delta: MouseScrollDelta) -> ScrollDelta {
|
||||
match delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => ScrollDelta::new(f64::from(x) * SCROLL_LINE_WIDTH, f64::from(y) * SCROLL_LINE_HEIGHT, 0.0),
|
||||
MouseScrollDelta::PixelDelta(position) => ScrollDelta::new(position.x, position.y, 0.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_modifiers() -> ModifierKeys {
|
||||
ModifierKeys::empty()
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
use std::sync::mpsc::{Sender, channel};
|
||||
|
||||
use graphite_editor::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use winit::cursor::CursorIcon;
|
||||
use winit::dpi::{LogicalPosition, Position};
|
||||
use winit::monitor::Fullscreen;
|
||||
use winit::window::{CursorGrabMode, Window};
|
||||
|
||||
pub enum WindowCommand {
|
||||
Minimize,
|
||||
Maximize,
|
||||
Fullscreen,
|
||||
Close,
|
||||
Hide,
|
||||
Focus,
|
||||
StartDrag,
|
||||
SetTitle(String),
|
||||
SetCursorIcon(CursorIcon),
|
||||
SetMouseCursor(MouseCursorIcon),
|
||||
PointerLock,
|
||||
PointerWarp { position: (f64, f64) },
|
||||
PointerUnlock,
|
||||
}
|
||||
|
||||
pub struct WindowCommandSender(pub Sender<WindowCommand>);
|
||||
|
||||
impl Clone for WindowCommandSender {
|
||||
fn clone(&self) -> Self {
|
||||
WindowCommandSender(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowCommandSender {
|
||||
pub fn send(&self, command: WindowCommand) {
|
||||
if let Err(error) = self.0.send(command) {
|
||||
tracing::warn!("dropped window command: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_pair() -> (WindowCommandSender, std::sync::mpsc::Receiver<WindowCommand>) {
|
||||
let (sender, receiver) = channel();
|
||||
(WindowCommandSender(sender), receiver)
|
||||
}
|
||||
|
||||
pub fn handle_command(window: &dyn Window, command: WindowCommand) {
|
||||
match command {
|
||||
WindowCommand::Minimize => window.set_minimized(true),
|
||||
WindowCommand::Maximize => {
|
||||
if window.fullscreen().is_some() {
|
||||
return;
|
||||
}
|
||||
window.set_maximized(!window.is_maximized());
|
||||
}
|
||||
WindowCommand::Fullscreen => {
|
||||
if window.fullscreen().is_some() {
|
||||
window.set_fullscreen(None);
|
||||
} else {
|
||||
window.set_fullscreen(Some(Fullscreen::Borderless(None)));
|
||||
}
|
||||
}
|
||||
WindowCommand::Close => {
|
||||
tracing::info!("window close requested");
|
||||
}
|
||||
WindowCommand::Hide => window.set_visible(false),
|
||||
WindowCommand::Focus => {
|
||||
window.set_minimized(false);
|
||||
window.focus_window();
|
||||
}
|
||||
WindowCommand::StartDrag => {
|
||||
if window.fullscreen().is_some() {
|
||||
return;
|
||||
}
|
||||
if let Err(error) = window.drag_window() {
|
||||
tracing::debug!("drag_window failed: {error}");
|
||||
}
|
||||
}
|
||||
WindowCommand::SetTitle(title) => window.set_title(&title),
|
||||
WindowCommand::SetCursorIcon(icon) => {
|
||||
window.set_cursor_visible(true);
|
||||
window.set_cursor(icon.into());
|
||||
}
|
||||
WindowCommand::SetMouseCursor(cursor) => apply_mouse_cursor(window, cursor),
|
||||
WindowCommand::PointerLock => {
|
||||
if window.set_cursor_grab(CursorGrabMode::Locked).is_err() {
|
||||
let _ = window.set_cursor_grab(CursorGrabMode::Confined);
|
||||
}
|
||||
window.set_cursor_visible(false);
|
||||
}
|
||||
WindowCommand::PointerWarp { position } => {
|
||||
let pos = Position::Logical(LogicalPosition::new(position.0, position.1));
|
||||
if let Err(error) = window.set_cursor_position(pos) {
|
||||
tracing::debug!("set_cursor_position failed: {error}");
|
||||
}
|
||||
}
|
||||
WindowCommand::PointerUnlock => {
|
||||
let _ = window.set_cursor_grab(CursorGrabMode::None);
|
||||
window.set_cursor_visible(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn translate_cursor(cursor: MouseCursorIcon) -> CursorIcon {
|
||||
match cursor {
|
||||
MouseCursorIcon::Default => CursorIcon::Default,
|
||||
MouseCursorIcon::None => CursorIcon::Default,
|
||||
MouseCursorIcon::ZoomIn => CursorIcon::ZoomIn,
|
||||
MouseCursorIcon::ZoomOut => CursorIcon::ZoomOut,
|
||||
MouseCursorIcon::Grabbing => CursorIcon::Grabbing,
|
||||
MouseCursorIcon::Crosshair => CursorIcon::Crosshair,
|
||||
MouseCursorIcon::Text => CursorIcon::Text,
|
||||
MouseCursorIcon::Move => CursorIcon::Move,
|
||||
MouseCursorIcon::NSResize => CursorIcon::NsResize,
|
||||
MouseCursorIcon::EWResize => CursorIcon::EwResize,
|
||||
MouseCursorIcon::NESWResize => CursorIcon::NeswResize,
|
||||
MouseCursorIcon::NWSEResize => CursorIcon::NwseResize,
|
||||
MouseCursorIcon::Rotate => CursorIcon::Alias,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_mouse_cursor(window: &dyn Window, cursor: MouseCursorIcon) {
|
||||
if matches!(cursor, MouseCursorIcon::None) {
|
||||
window.set_cursor_visible(false);
|
||||
return;
|
||||
}
|
||||
window.set_cursor_visible(true);
|
||||
window.set_cursor(translate_cursor(cursor).into());
|
||||
}
|
||||
|
||||
pub fn format_window_title(active_doc_name: Option<&str>) -> String {
|
||||
match active_doc_name {
|
||||
Some(name) if !name.is_empty() => format!("{name} \u{2013} Graphite"),
|
||||
_ => String::from("Graphite"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drain_into<F: FnMut(WindowCommand)>(receiver: &std::sync::mpsc::Receiver<WindowCommand>, mut apply: F) {
|
||||
while let Ok(command) = receiver.try_recv() {
|
||||
apply(command);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue