Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First prototype of msvg #68

Merged
merged 2 commits into from
Dec 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ whiskers-derive = { path = "crates/whiskers-derive", version = "=0.3.0-alpha.0"
# dependencies
anyhow = "1"
bumpalo = "3.14.0" # avoid yanked 3.12.1, pulled by wasm-bindgen
camino = "1.1.0"
convert_case = "0.6.0"
criterion = "0.5.1"
eframe = { version = "0.24.1", default-features = false, features = [
Expand Down
6 changes: 6 additions & 0 deletions crates/msvg/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,11 @@ publish = false # dont publish for now...
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
vsvg.workspace = true
vsvg-viewer.workspace = true

anyhow.workspace = true
camino.workspace = true
egui.workspace = true
eframe.workspace = true
rayon.workspace = true
25 changes: 23 additions & 2 deletions crates/msvg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,29 @@

This crate is part of the [*vsvg* project](https://github.com/abey79/vsvg).

**Status**: On the TODO list
**Status**: prototyping stage, aiming toward MVP.

## What's this?

Compared to *vpype*, *vsvg* is *extremely* fast to load and display SVGs. This opens up the prospect of a tool that can load a whole bunch of SVGs (e.g. many realisations of a generative art algorithm) and quickly browse/display them (e.g. to pick up the best realisation). This is what *msvg* will be. Currently, it's just an empty shell though.
Compared to *vpype*, *vsvg* is *extremely* fast to load and display SVGs. This makes a tool that can load a whole bunch of SVGs possible, for example to chose amongst many realisations of a generative art algorithm. This is what *msvg* aims to be.

## Installation

**WARNING**: this is at an early prototype stage!

To install `msvg`, you'll need Rust, which you can install using [`rustup`](https://www.rust-lang.org/learn/get-started).

Then, run the following command:

```
cargo install --git https://github.com/abey79/vsvg msvg
```


## Usage

```
msvg PATH [PATH...]
```

`PATH` may be a SVG file or a directory. If it is a directory, it will be recursively traversed and all founds will be included.
231 changes: 207 additions & 24 deletions crates/msvg/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,220 @@
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::missing_errors_doc)]

mod multi_viewer;
use camino::Utf8PathBuf;
use eframe::CreationContext;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use vsvg::Document;
use vsvg_viewer::{show_with_viewer_app, DocumentWidget, ViewerApp};

use crate::multi_viewer::MultiViewer;
use std::error::Error;
use std::path::PathBuf;
enum LoadingMessage {
Starting(Utf8PathBuf),
Loaded(Utf8PathBuf, Arc<Document>),
//TODO: error state
Completed,
}

#[derive(Default, Debug)]
enum LoadedDocument {
#[default]
Pending,
Loading,
Loaded(Arc<Document>),
// TODO: add error state: Error(Box<dyn Error + Send>),
// TODO: Document::from_svg() should return an proper error type
}

struct MsvgViewerApp {
/// The list of paths to be loaded.
paths: Vec<Utf8PathBuf>,

/// The loaded documents.
loaded_documents: BTreeMap<Utf8PathBuf, LoadedDocument>,

/// The currently selected document.
active_document: usize,

/// The channel rx
rx: std::sync::mpsc::Receiver<LoadingMessage>,

/// Are loading messages still coming in?
///
/// UI keeps refreshing until this is false.
waiting_for_messages: bool,
}

impl MsvgViewerApp {
pub fn from_paths(paths: Vec<Utf8PathBuf>) -> Self {
let loaded_documents: BTreeMap<_, _> = paths
.into_iter()
.map(|path| (path, LoadedDocument::Pending))
.collect();

// make sure they are in the same order
let paths: Vec<_> = loaded_documents.keys().cloned().collect();

let (sender, rx) = std::sync::mpsc::channel::<LoadingMessage>();

paths.clone().into_iter().for_each(|path| {
let sender = sender.clone();
rayon::spawn_fifo(move || {
sender.send(LoadingMessage::Starting(path.clone())).unwrap();
let document = Document::from_svg(&path, false).unwrap();
sender
.send(LoadingMessage::Loaded(path.clone(), Arc::new(document)))
.unwrap();
});
});
rayon::spawn_fifo(move || {
sender.send(LoadingMessage::Completed).unwrap();
});

Self {
paths,
loaded_documents,
active_document: 0,
rx,
waiting_for_messages: true,
}
}
}

impl ViewerApp for MsvgViewerApp {
fn setup(
&mut self,
_cc: &CreationContext,
_document_widget: &mut DocumentWidget,
) -> anyhow::Result<()> {
Ok(())
}

fn show(paths: Vec<PathBuf>) -> Result<(), Box<dyn Error>> {
let native_options = eframe::NativeOptions::default();
fn update(
&mut self,
ctx: &egui::Context,
document_widget: &mut DocumentWidget,
) -> anyhow::Result<()> {
let mut document_dirty = false;

eframe::run_native(
"vsvg multi",
native_options,
Box::new(move |cc| {
let style = egui::Style {
visuals: egui::Visuals::light(),
..egui::Style::default()
};
cc.egui_ctx.set_style(style);
for msg in self.rx.try_iter() {
match msg {
LoadingMessage::Starting(path) => {
self.loaded_documents
.entry(path)
.and_modify(|state| *state = LoadedDocument::Loading);
}
LoadingMessage::Loaded(path, doc) => {
// find index into paths vec
if self.paths.iter().position(|p| p == &path) == Some(self.active_document) {
document_dirty = true;
}

Box::new(MultiViewer::new(paths))
}),
)?;
self.loaded_documents
.entry(path)
.and_modify(|state| *state = LoadedDocument::Loaded(doc));
}
LoadingMessage::Completed => {
self.waiting_for_messages = false;
}
}
}

if self.waiting_for_messages {
ctx.request_repaint();
}

egui::SidePanel::right("right_panel")
.default_width(200.)
.show(ctx, |ui| {
egui::ScrollArea::both().show(ui, |ui| {
ctx.input(|i| {
if i.key_pressed(egui::Key::ArrowUp) {
self.active_document = self.active_document.saturating_sub(1);
document_dirty = true;
}

if i.key_pressed(egui::Key::ArrowDown)
&& self.active_document < self.loaded_documents.len() - 1
{
self.active_document = self.active_document.saturating_add(1);
document_dirty = true;
}
});

for (i, path) in self.paths.iter().enumerate() {
let Some(state) = self.loaded_documents.get(path) else {
continue;
};

let file_name = path.file_name().map(ToOwned::to_owned).unwrap_or_default();

match state {
LoadedDocument::Pending => {
ui.weak(file_name);
}

LoadedDocument::Loading => {
ui.horizontal(|ui| {
ui.weak(file_name);
ui.spinner();
});
}
LoadedDocument::Loaded(document) => {
if ui
.selectable_label(self.active_document == i, file_name)
.clicked()
{
self.active_document = i;
document_dirty = true;
}

if document_dirty && self.active_document == i {
document_widget.set_document(document.clone());
document_dirty = false;
}
}
}
}
});
});

Ok(())
}
}

fn visit_file(file: PathBuf, paths: &mut Vec<Utf8PathBuf>) -> anyhow::Result<()> {
if file.extension() == Some("svg".as_ref()) {
paths.push(file.try_into()?);
}

Ok(())
}

fn visit_dir(dir: &Path, paths: &mut Vec<Utf8PathBuf>) -> anyhow::Result<()> {
for entry in dir.read_dir()? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
visit_dir(&path, paths)?;
} else {
visit_file(path, paths)?;
}
}

Ok(())
}

fn main() -> Result<(), Box<dyn Error>> {
let paths = std::env::args()
.skip(1)
.map(PathBuf::from)
.collect::<Vec<_>>();
fn main() -> anyhow::Result<()> {
let mut svg_list = Vec::new();

for path in std::env::args().skip(1).map(PathBuf::from) {
if path.is_dir() {
visit_dir(&path, &mut svg_list)?;
} else {
visit_file(path, &mut svg_list)?;
}
}

show(paths)
show_with_viewer_app(MsvgViewerApp::from_paths(svg_list))
}
43 changes: 0 additions & 43 deletions crates/msvg/src/multi_viewer.rs

This file was deleted.