Compare commits

..

No commits in common. "master" and "v0.1" have entirely different histories.
master ... v0.1

11 changed files with 224 additions and 735 deletions

116
Cargo.lock generated
View File

@ -162,38 +162,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.0.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim",
"termcolor",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "clap_derive"
version = "3.0.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "cmake" name = "cmake"
version = "0.1.45" version = "0.1.45"
@ -370,7 +338,7 @@ checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
[[package]] [[package]]
name = "druid" name = "druid"
version = "0.7.0" version = "0.7.0"
source = "git+https://github.com/doppioandante/druid?branch=v0.7.0_stiletto#2cbb14456fb1813e673d2151626630d2c4db68bf" source = "git+https://github.com/doppioandante/druid?branch=v0.7.0_stiletto#f0a6fd3f6bece1b1d10e261ce57673d7f6f1969a"
dependencies = [ dependencies = [
"console_log", "console_log",
"druid-derive", "druid-derive",
@ -393,7 +361,7 @@ dependencies = [
[[package]] [[package]]
name = "druid-derive" name = "druid-derive"
version = "0.4.0" version = "0.4.0"
source = "git+https://github.com/doppioandante/druid?branch=v0.7.0_stiletto#2cbb14456fb1813e673d2151626630d2c4db68bf" source = "git+https://github.com/doppioandante/druid?branch=v0.7.0_stiletto#f0a6fd3f6bece1b1d10e261ce57673d7f6f1969a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -403,7 +371,7 @@ dependencies = [
[[package]] [[package]]
name = "druid-shell" name = "druid-shell"
version = "0.7.0" version = "0.7.0"
source = "git+https://github.com/doppioandante/druid?branch=v0.7.0_stiletto#2cbb14456fb1813e673d2151626630d2c4db68bf" source = "git+https://github.com/doppioandante/druid?branch=v0.7.0_stiletto#f0a6fd3f6bece1b1d10e261ce57673d7f6f1969a"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags", "bitflags",
@ -884,12 +852,6 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.3.2" version = "0.3.2"
@ -922,16 +884,6 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "indexmap"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.9" version = "0.1.9"
@ -1082,12 +1034,6 @@ version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
[[package]]
name = "os_str_bytes"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
[[package]] [[package]]
name = "pango" name = "pango"
version = "0.9.1" version = "0.9.1"
@ -1397,15 +1343,6 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "serde_bare"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01db2255aa98fb93ad74272d8b2e6fd4851860e733e944b9439cf148127164b2"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.117" version = "1.0.117"
@ -1550,7 +1487,6 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
name = "stiletto" name = "stiletto"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap",
"druid", "druid",
"gdk", "gdk",
"gio", "gio",
@ -1558,16 +1494,9 @@ dependencies = [
"im", "im",
"log", "log",
"serde", "serde",
"serde_bare",
"serde_json", "serde_json",
] ]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.18.0" version = "0.18.0"
@ -1622,24 +1551,6 @@ dependencies = [
"version-compare", "version-compare",
] ]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789"
dependencies = [
"unicode-width",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.24" version = "1.0.24"
@ -1848,12 +1759,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
[[package]]
name = "unicode-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.1" version = "0.2.1"
@ -1902,12 +1807,6 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]] [[package]]
name = "version-compare" name = "version-compare"
version = "0.0.10" version = "0.0.10"
@ -2006,15 +1905,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"

View File

@ -3,30 +3,27 @@ name = "stiletto"
version = "0.1.0" version = "0.1.0"
authors = ["Enrico Lumetti <enrico.lumetti@gmail.com>"] authors = ["Enrico Lumetti <enrico.lumetti@gmail.com>"]
edition = "2018" edition = "2018"
default-run = "stiletto"
[dependencies] [dependencies]
log = "0.4" log = "0.4"
druid = { version = "0.7.0", features = ["im", "svg"] } druid = { version = "0.7.0", features = ["im", "svg"] }
im = { version = "*" } im = { version = "*" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_bare = "0.3.0" #serde_bare = "0.3.0"
serde_json = "1.0" serde_json = "1.0"
clap = "3.0.0-beta.2"
[target.'cfg(target_os="linux")'.dependencies.gtk]
version = "0.9.2"
features = ["v3_22"]
[target.'cfg(target_os="linux")'.dependencies.gio]
version = "0.9.1"
features = ["v2_56"]
[target.'cfg(target_os="linux")'.dependencies.gdk]
version = "0.13.2"
features = ["v3_22"]
[patch.crates-io] [patch.crates-io]
druid = { git = "https://github.com/doppioandante/druid", branch = "v0.7.0_stiletto", features = ["im", "svg"] } druid = { git = "https://github.com/doppioandante/druid", branch = "v0.7.0_stiletto", features = ["im", "svg"] }
#druid = { path = "../druid/druid/", features = ["im", "svg"] } #druid = { path = "../druid/druid/", features = ["im", "svg"] }
[dependencies.gtk]
version = "0.9.2"
features = ["v3_22"]
[dependencies.gio]
version = "0.9.1"
features = ["v2_56"]
[dependencies.gdk]
version = "0.13.2"
features = ["v3_22"]

View File

@ -1,48 +0,0 @@
// Stiletto
// Copyright (C) 2020 Stiletto Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use clap::{Arg, App};
use stiletto::migration::open_stiletto_document;
use std::fs::File;
use std::path::PathBuf;
fn main() {
let matches = App::new("Stiletto Migration CLI")
.version("0.1.0")
.author("Stiletto Authors")
.about("Migrate a stlt file to the latest version")
.arg(Arg::new("INPUT")
.about("Sets the input file to use")
.required(true)
.index(1))
.arg(Arg::new("OUTPUT")
.about("Sets the output file to use")
.required(true)
.index(2))
.get_matches();
let input_path = matches.value_of("INPUT").unwrap();
let output_path = matches.value_of("OUTPUT").unwrap();
let out_file = File::create(&output_path).expect("Cannot create output fle");
let result = open_stiletto_document(&PathBuf::from(&input_path));
if let Ok(document) = result {
let snapshot = document.migrate_to_latest();
snapshot.to_writer(out_file);
}
}

View File

@ -16,7 +16,7 @@
use std::vec::Vec; use std::vec::Vec;
use im::{vector, Vector}; use im::Vector;
use serde::de::{Deserializer, SeqAccess, Visitor}; use serde::de::{Deserializer, SeqAccess, Visitor};
use serde::ser::Serializer; use serde::ser::Serializer;
@ -128,43 +128,20 @@ impl CanvasElement {
} }
} }
} }
pub fn get_path_mut(&mut self) -> Option<&mut Path> {
match self {
CanvasElement::Freehand { path, .. } => Some(path),
}
}
} }
#[derive(Clone, druid::Data)] #[derive(Clone, druid::Data)]
pub struct Canvas { pub struct Canvas {
elements: Vector<CanvasElement>, pub elements: Vector<CanvasElement>,
content_size: druid::Size,
} }
impl Canvas { impl Canvas {
pub fn new() -> Self {
Canvas {
elements: vector![],
content_size: druid::Size::new(0.0, 0.0),
}
}
pub fn new_with_elements(elements: Vector<CanvasElement>) -> Self {
let mut cv = Canvas::new();
for e in elements {
cv.add_element(e);
}
cv
}
pub fn add_element(&mut self, element: CanvasElement) {
self.content_size = self
.content_size
.to_rect()
.union(element.bounding_box())
.size();
self.elements.push_back(element);
}
pub fn elements(&self) -> &Vector<CanvasElement> {
&self.elements
}
/// Find all CanvasElement that intersect with rect /// Find all CanvasElement that intersect with rect
pub fn find_intersections(&self, rect: druid::Rect) -> Vec<usize> { pub fn find_intersections(&self, rect: druid::Rect) -> Vec<usize> {
let mut found_elements = Vec::<usize>::new(); let mut found_elements = Vec::<usize>::new();
@ -180,10 +157,6 @@ impl Canvas {
found_elements found_elements
} }
pub fn content_size(&self) -> druid::Size {
self.content_size
}
} }
impl Serialize for Path { impl Serialize for Path {
@ -197,7 +170,7 @@ impl Serialize for Path {
PathEl::MoveTo(pt) => Some(Into::<(f64, f64)>::into(pt)), PathEl::MoveTo(pt) => Some(Into::<(f64, f64)>::into(pt)),
PathEl::LineTo(pt) => Some(Into::<(f64, f64)>::into(pt)), PathEl::LineTo(pt) => Some(Into::<(f64, f64)>::into(pt)),
_ => None, _ => None,
}).collect::<Vec<(f64, f64)>>()) }))
} }
} }

View File

@ -21,28 +21,16 @@ pub mod canvas;
pub mod history; pub mod history;
pub mod tool; pub mod tool;
pub mod widget; pub mod widget;
pub mod migration;
pub mod commands { pub mod commands {
use druid::Selector; use druid::Selector;
pub const UPDATE_TOOL: Selector = Selector::new("stiletto_update_tool"); pub const UPDATE_TOOL: Selector = Selector::new("stiletto_update_tool");
pub const PUSH_ERASER: Selector<()> = Selector::new("push_eraser_context");
pub const POP_ERASER: Selector<()> = Selector::new("pop_eraser_context");
} }
pub const STILETTO_FORMAT_MAJOR: u16 = 0;
pub const STILETTO_FORMAT_MINOR: u16 = 2;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct DocumentSnapshot { pub struct DocumentSnapshot {
pub format_version_major: u16, pub format_version_major: u16,
pub format_version_minor: u16, pub format_version_minor: u16,
pub canvas_elements: Vec<canvas::CanvasElement>, pub canvas_elements: Vec<canvas::CanvasElement>,
} }
impl DocumentSnapshot {
pub fn to_writer<W: std::io::Write>(&self, writer: W) -> Result<(), serde_bare::Error> {
serde_bare::to_writer(writer, &self)
}
}

View File

@ -14,39 +14,31 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::path::PathBuf;
use log::{info, warn}; use log::{info, warn};
use druid::commands; use druid::commands;
use druid::im::vector; use druid::im::vector;
use druid::widget::prelude::*; use druid::widget::prelude::*;
use druid::widget::{ use druid::widget::{Align, Button, CrossAxisAlignment, Flex, List, SizedBox, WidgetExt};
Align, Button, Controller, CrossAxisAlignment, Flex, List, SizedBox, WidgetExt,
};
use druid::{ use druid::{
AppDelegate, AppLauncher, Color, Command, Data, DelegateCtx, Env, FileDialogOptions, FileSpec, AppDelegate, AppLauncher, Color, Command, Data, DelegateCtx, Env, FileDialogOptions, FileSpec,
Handled, Lens, Target, WindowDesc, Handled, Lens, LocalizedString, Target, WindowDesc,
}; };
use im::Vector; use im::Vector;
use stiletto::tool::CanvasToolParams; use stiletto::canvas::Canvas;
use stiletto::widget::tool_ctx::{CanvasToolCtx}; use stiletto::history::VersionedCanvas;
use stiletto::tool::{CanvasToolCtx, CanvasToolParams};
use stiletto::widget::{build_simple_tool_widget, CanvasState, CanvasToolIconState, CanvasWidget}; use stiletto::widget::{build_simple_tool_widget, CanvasState, CanvasToolIconState, CanvasWidget};
use stiletto::DocumentSnapshot; use stiletto::DocumentSnapshot;
pub fn main() { pub fn main() {
let window = WindowDesc::new(build_ui) let window = WindowDesc::new(build_ui)
.window_size((1024.0, 1400.0)) .window_size((1024.0, 1400.0))
.title(|data: &StilettoState, _env: &Env| { .title(
let doc_name = if let Some(path) = &data.current_file_path { LocalizedString::new("custom-widget-demo-window-title").with_placeholder("Stiletto"),
String::from(path.to_string_lossy()) );
} else {
String::from("New Document")
};
format!("Stiletto - {}", doc_name)
});
let default_pen_params = CanvasToolParams::Pen { let default_pen_params = CanvasToolParams::Pen {
thickness: 2.0, thickness: 2.0,
@ -57,7 +49,12 @@ pub fn main() {
color: Color::rgb(255.0, 0.0, 0.0), color: Color::rgb(255.0, 0.0, 0.0),
}; };
let canvas_data = StilettoState { let canvas_data = StilettoState {
canvas: CanvasState::new(CanvasToolCtx::new(default_pen_params.clone())), canvas: CanvasState {
versioned_canvas: VersionedCanvas::new(Canvas {
elements: vector![],
}),
tool_ctx: CanvasToolCtx::new(default_pen_params.clone()),
},
tool_icons: vector![ tool_icons: vector![
CanvasToolIconState { CanvasToolIconState {
tool_params: default_pen_params, tool_params: default_pen_params,
@ -73,8 +70,6 @@ pub fn main() {
}, },
], ],
current_tool: 0, current_tool: 0,
eraser_tool_id: 2,
current_file_path: None,
}; };
AppLauncher::with_window(window) AppLauncher::with_window(window)
.use_simple_logger() .use_simple_logger()
@ -88,22 +83,6 @@ struct StilettoState {
canvas: CanvasState, canvas: CanvasState,
tool_icons: Vector<CanvasToolIconState>, tool_icons: Vector<CanvasToolIconState>,
current_tool: usize, current_tool: usize,
eraser_tool_id: usize,
#[data(same_fn = "PartialEq::eq")]
current_file_path: Option<PathBuf>,
}
impl StilettoState {
pub fn set_tool(&mut self, new_tool: usize) {
let old_tool = self.current_tool;
info!("Changing active tool to: tool #{}", new_tool);
self.tool_icons.get_mut(old_tool).unwrap().selected = false;
self.tool_icons.get_mut(new_tool).unwrap().selected = true;
self.current_tool = new_tool;
self.canvas.set_tool_ctx(CanvasToolCtx::new(
self.tool_icons.get(new_tool).unwrap().tool_params.clone(),
));
}
} }
fn build_ui() -> impl Widget<StilettoState> { fn build_ui() -> impl Widget<StilettoState> {
@ -120,37 +99,23 @@ fn build_ui() -> impl Widget<StilettoState> {
let save_dialog_options = FileDialogOptions::new() let save_dialog_options = FileDialogOptions::new()
.allowed_types(vec![stlt]) .allowed_types(vec![stlt])
.default_type(stlt); .default_type(stlt);
let save_dialog_options_clone = save_dialog_options.clone();
let open_dialog_options = save_dialog_options.clone(); let open_dialog_options = save_dialog_options.clone();
let save_buttons = Flex::row() let save_buttons = Flex::row()
.cross_axis_alignment(CrossAxisAlignment::Center) .cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(
Button::new("Save").on_click(move |ctx, data: &mut StilettoState, _| {
if data.current_file_path.is_some() {
ctx.submit_command(Command::new(commands::SAVE_FILE, (), Target::Auto));
} else {
ctx.submit_command(Command::new(
druid::commands::SHOW_SAVE_PANEL,
save_dialog_options_clone.clone(),
Target::Auto,
))
}
}),
)
.with_child(Button::new("Save As").on_click(move |ctx, _, _| {
ctx.submit_command(Command::new(
druid::commands::SHOW_SAVE_PANEL,
save_dialog_options.clone(),
Target::Auto,
))
}))
.with_child(Button::new("Open").on_click(move |ctx, _, _| { .with_child(Button::new("Open").on_click(move |ctx, _, _| {
ctx.submit_command(Command::new( ctx.submit_command(Command::new(
druid::commands::SHOW_OPEN_PANEL, druid::commands::SHOW_OPEN_PANEL,
open_dialog_options.clone(), open_dialog_options.clone(),
Target::Auto, Target::Auto,
)) ))
}))
.with_child(Button::new("Save").on_click(move |ctx, _, _| {
ctx.submit_command(Command::new(
druid::commands::SHOW_SAVE_PANEL,
save_dialog_options.clone(),
Target::Auto,
))
})); }));
let tool_buttons = Flex::row() let tool_buttons = Flex::row()
@ -161,7 +126,7 @@ fn build_ui() -> impl Widget<StilettoState> {
.padding((8.0, 0.0)) .padding((8.0, 0.0))
.on_click(|ctx, tool_state, _| { .on_click(|ctx, tool_state, _| {
tool_state.selected = true; tool_state.selected = true;
ctx.submit_notification(stiletto::commands::UPDATE_TOOL); ctx.submit_command(stiletto::commands::UPDATE_TOOL);
}) })
}) })
.horizontal() .horizontal()
@ -183,57 +148,7 @@ fn build_ui() -> impl Widget<StilettoState> {
.cross_axis_alignment(CrossAxisAlignment::Center) .cross_axis_alignment(CrossAxisAlignment::Center)
.must_fill_main_axis(true) .must_fill_main_axis(true)
.with_child(SizedBox::new(Align::left(toolbar)).height(50.0)) .with_child(SizedBox::new(Align::left(toolbar)).height(50.0))
.with_flex_child((CanvasWidget).lens(StilettoState::canvas), 1.0) .with_flex_child((CanvasWidget {}).lens(StilettoState::canvas), 1.0)
.controller(ToolSwitcher::new())
}
struct ToolSwitcher {
saved_tool: usize,
}
impl ToolSwitcher {
pub fn new() -> Self {
ToolSwitcher { saved_tool: 0 }
}
}
impl<W: Widget<StilettoState>> Controller<StilettoState, W> for ToolSwitcher {
fn event(
&mut self,
child: &mut W,
ctx: &mut EventCtx,
event: &Event,
data: &mut StilettoState,
env: &Env,
) {
match event {
Event::Notification(cmd) => {
let new_tool = if cmd.get(stiletto::commands::PUSH_ERASER).is_some() {
ctx.set_handled();
self.saved_tool = data.current_tool;
Some(data.eraser_tool_id)
} else if cmd.get(stiletto::commands::POP_ERASER).is_some() {
ctx.set_handled();
Some(self.saved_tool)
} else if cmd.get(stiletto::commands::UPDATE_TOOL).is_some() {
ctx.set_handled();
let old_tool = data.current_tool;
data.tool_icons
.iter()
.enumerate()
.position(|(pos, x)| pos != old_tool && x.selected)
} else {
None
};
if let Some(new_tool) = new_tool {
data.set_tool(new_tool);
}
}
_ => {}
}
child.event(ctx, event, data, env);
}
} }
struct Delegate; struct Delegate;
@ -249,23 +164,15 @@ impl AppDelegate<StilettoState> for Delegate {
) -> Handled { ) -> Handled {
use std::fs::File; use std::fs::File;
if cmd.get(commands::SAVE_FILE_AS).is_some() || cmd.get(commands::SAVE_FILE).is_some() { if let Some(file_info) = cmd.get(commands::SAVE_FILE_AS) {
let mut path_buf = cmd let res_file = File::create(file_info.path());
.get(commands::SAVE_FILE_AS)
.map(|file_info| file_info.path().to_path_buf())
.or_else(|| data.current_file_path.as_ref().cloned())
.unwrap();
path_buf.set_extension("stlt");
let res_file = File::create(&path_buf);
if let Ok(f) = res_file { if let Ok(f) = res_file {
let write_res = let write_res =
data.canvas.get_document_snapshot().to_writer(f); serde_json::to_writer_pretty(f, &data.canvas.get_document_snapshot());
if write_res.is_err() { if write_res.is_err() {
warn!("Error while saving: {:?}", write_res.err()); warn!("Error while saving: {:?}", write_res.err());
} else { } else {
info!("Written to file: {}", &path_buf.display()); info!("Written to file: {}", file_info.path().display());
data.current_file_path = Some(path_buf);
} }
} else { } else {
warn!("Cannot create file: {:?}", res_file.err()); warn!("Cannot create file: {:?}", res_file.err());
@ -274,22 +181,33 @@ impl AppDelegate<StilettoState> for Delegate {
} }
if let Some(file_info) = cmd.get(commands::OPEN_FILE) { if let Some(file_info) = cmd.get(commands::OPEN_FILE) {
if let Ok(f) = File::open(file_info.path()) { if let Ok(f) = File::open(file_info.path()) {
let res_snapshot: Result<DocumentSnapshot, serde_bare::Error> = let res_snapshot: Result<DocumentSnapshot, serde_json::Error> =
serde_bare::from_reader(f); serde_json::from_reader(f);
if let Ok(document_snapshot) = res_snapshot { if let Ok(document_snapshot) = res_snapshot {
data.canvas.set_from_snapshot(document_snapshot); data.canvas.set_from_snapshot(document_snapshot);
data.current_file_path = Some(file_info.path().to_path_buf());
info!("Loaded file {}", file_info.path().display()); info!("Loaded file {}", file_info.path().display());
} else { } else {
warn!( warn!("Error while loading {}: {:?}", file_info.path().display(), res_snapshot.err());
"Error while loading {}: {:?}",
file_info.path().display(),
res_snapshot.err()
);
} }
} }
return Handled::Yes; return Handled::Yes;
} }
if cmd.get(stiletto::commands::UPDATE_TOOL).is_some() {
let old_tool = data.current_tool;
if let Some(new_tool) = data
.tool_icons
.iter()
.enumerate()
.position(|(pos, x)| pos != old_tool && x.selected)
{
info!("Changing active tool to: tool #{}", new_tool);
data.tool_icons.get_mut(old_tool).unwrap().selected = false;
data.current_tool = new_tool;
data.canvas.set_tool_ctx(CanvasToolCtx::new(
data.tool_icons.get(new_tool).unwrap().tool_params.clone(),
));
}
}
Handled::No Handled::No
} }
} }

View File

@ -1,142 +0,0 @@
// Stiletto
// Copyright (C) 2020 Stiletto Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::{DocumentSnapshot, STILETTO_FORMAT_MAJOR, STILETTO_FORMAT_MINOR};
use std::error::Error;
use std::fmt;
use std::fs::File;
use std::io;
use std::path::PathBuf;
#[derive(Debug)]
pub enum MigrationError {
SerdeJsonError(serde_json::Error),
SerdeBareError(serde_bare::Error),
IoError(io::Error),
UnexpectedVersion(u16, u16, u16, u16),
}
#[derive(Debug)]
pub enum StilettoDocument {
DocumentSnapshot0_1(DocumentSnapshot),
DocumentSnapshot0_2(DocumentSnapshot),
}
impl StilettoDocument {
pub fn version(&self) -> (u16, u16) {
match &self {
StilettoDocument::DocumentSnapshot0_1(_) => (0, 1),
StilettoDocument::DocumentSnapshot0_2(_) => (0, 2),
}
}
pub fn migrate_to_next(self) -> StilettoDocument {
match self {
StilettoDocument::DocumentSnapshot0_1(snapshot) => StilettoDocument::DocumentSnapshot0_2(snapshot) ,
StilettoDocument::DocumentSnapshot0_2(_) => self,
}
}
pub fn migrate_to_latest(mut self) -> DocumentSnapshot {
while self.version() != (STILETTO_FORMAT_MAJOR, STILETTO_FORMAT_MINOR) {
self = self.migrate_to_next()
}
assert!(matches!(self, StilettoDocument::DocumentSnapshot0_2(_)));
match self {
StilettoDocument::DocumentSnapshot0_2(snapshot) => snapshot,
_ => panic!("Wrong Document Snapshot Version")
}
}
}
impl fmt::Display for MigrationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
MigrationError::SerdeJsonError(e) => e.fmt(f),
MigrationError::SerdeBareError(e) => e.fmt(f),
MigrationError::IoError(e) => e.fmt(f),
MigrationError::UnexpectedVersion(major, minor, e_major, e_minor) => {
write!(
f, "Unexpected version ({}, {}): was expecting ({}, {})",
major, minor, e_major, e_minor
)
},
}
}
}
impl Error for MigrationError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
MigrationError::SerdeJsonError(e) => e.source(),
MigrationError::SerdeBareError(e) => e.source(),
MigrationError::IoError(e) => e.source(),
MigrationError::UnexpectedVersion(..) => None,
}
}
}
impl From<io::Error> for MigrationError {
fn from(error: io::Error) -> Self {
MigrationError::IoError(error)
}
}
impl From<serde_json::Error> for MigrationError {
fn from(error: serde_json::Error) -> Self {
MigrationError::SerdeJsonError(error)
}
}
impl From<serde_bare::Error> for MigrationError {
fn from(error: serde_bare::Error) -> Self {
MigrationError::SerdeBareError(error)
}
}
fn open_0_1(path: &PathBuf) -> Result<StilettoDocument, MigrationError> {
let f = File::open(path)?;
let document_snapshot: DocumentSnapshot = serde_json::from_reader(f)?;
if document_snapshot.format_version_major != 0 ||
document_snapshot.format_version_minor != 1 {
Err(MigrationError::UnexpectedVersion(
document_snapshot.format_version_major,
document_snapshot.format_version_minor,
0, 1
))
} else {
Ok(StilettoDocument::DocumentSnapshot0_1(document_snapshot))
}
}
fn open_0_2(path: &PathBuf) -> Result<StilettoDocument, MigrationError> {
let f = File::open(path)?;
let document_snapshot: DocumentSnapshot = serde_bare::from_reader(f)?;
Ok(StilettoDocument::DocumentSnapshot0_2(document_snapshot))
}
pub fn open_stiletto_document(path: &PathBuf) -> Result<StilettoDocument, MigrationError> {
if let Ok(res) = open_0_1(path) {
return Ok(res);
}
open_0_2(path)
}

View File

@ -14,7 +14,12 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use druid::{Color, Data}; use druid::kurbo::BezPath;
use druid::{Color, Data, Env, Event, EventCtx, PaintCtx};
use crate::canvas;
use crate::canvas::{Canvas, CanvasElement};
use crate::history::VersionedCanvas;
#[derive(Clone, Data)] #[derive(Clone, Data)]
pub enum CanvasToolParams { pub enum CanvasToolParams {
@ -28,3 +33,137 @@ pub enum CanvasToolType {
Eraser, Eraser,
} }
#[derive(Clone, Data)]
pub enum CanvasToolState {
Idle,
DrawingFreehand {
pen_params: CanvasToolParams,
current_path: CanvasElement,
},
Erasing,
}
#[derive(Clone, Data)]
pub struct CanvasToolCtx {
initial_params: CanvasToolParams,
state: CanvasToolState,
}
impl CanvasToolParams {
pub fn tool_type(&self) -> CanvasToolType {
match self {
CanvasToolParams::Pen { .. } => CanvasToolType::Pen,
CanvasToolParams::Eraser => CanvasToolType::Eraser,
}
}
}
impl CanvasToolCtx {
pub fn new(params: CanvasToolParams) -> Self {
CanvasToolCtx {
initial_params: params,
state: CanvasToolState::Idle,
}
}
pub fn handle_event(
&mut self,
ctx: &EventCtx,
event: &Event,
mut vcanvas: &mut VersionedCanvas,
env: &Env,
) {
match self.initial_params.tool_type() {
CanvasToolType::Pen => self.handle_pen_event(&ctx, &event, &mut vcanvas, &env),
CanvasToolType::Eraser => self.handle_erase_event(&ctx, &event, &mut vcanvas, &env),
}
}
pub fn handle_erase_event(
&mut self,
_ctx: &EventCtx,
event: &Event,
vcanvas: &mut VersionedCanvas,
_env: &Env,
) {
match (&mut self.state, event) {
(CanvasToolState::Idle, Event::MouseDown(_)) => {
self.state = CanvasToolState::Erasing;
}
(CanvasToolState::Erasing, Event::MouseMove(mouse_event)) => {
let eraser_rect = druid::Rect::from_center_size(mouse_event.pos, (5.0, 5.0));
let elements = vcanvas.get().find_intersections(eraser_rect);
if !elements.is_empty() {
vcanvas.update(|canvas: &mut Canvas| {
for i in elements {
canvas.elements.remove(i);
}
});
}
}
(CanvasToolState::Erasing, Event::MouseUp(_)) => {
self.state = CanvasToolState::Idle;
}
_ => {}
}
}
pub fn handle_pen_event(
&mut self,
_ctx: &EventCtx,
event: &Event,
vcanvas: &mut VersionedCanvas,
_env: &Env,
) {
match (&mut self.state, event) {
(CanvasToolState::Idle, Event::MouseDown(mouse_event)) => {
let mut kurbo_path = BezPath::new();
kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y));
if let CanvasToolParams::Pen { thickness, color } = &self.initial_params {
self.state = CanvasToolState::DrawingFreehand {
pen_params: self.initial_params.clone(),
current_path: CanvasElement::Freehand {
path: canvas::Path { kurbo_path },
thickness: *thickness,
stroke_color: color.clone(),
},
};
}
}
(
CanvasToolState::DrawingFreehand {
ref mut current_path,
..
},
Event::MouseMove(mouse_event),
) => {
current_path
.get_path_mut()
.unwrap()
.kurbo_path
.line_to((mouse_event.pos.x, mouse_event.pos.y));
}
(CanvasToolState::DrawingFreehand { .. }, Event::MouseUp(_)) => {
vcanvas.update(move |canvas: &mut Canvas| {
let current_state = std::mem::replace(&mut self.state, CanvasToolState::Idle);
if let CanvasToolState::DrawingFreehand { current_path, .. } = current_state {
canvas.elements.push_back(current_path);
}
});
}
_ => {}
}
}
pub fn needs_repaint(&self) -> bool {
true
}
pub fn paint(&self, ctx: &mut PaintCtx, _env: &Env) {
match &self.state {
CanvasToolState::DrawingFreehand { current_path, .. } => current_path.draw(ctx),
_ => {}
}
}
}

View File

@ -16,29 +16,21 @@
use im::Vector; use im::Vector;
use super::tool_ctx::{CanvasToolCtx};
use crate::canvas::Canvas; use crate::canvas::Canvas;
use crate::history::VersionedCanvas; use crate::history::VersionedCanvas;
use crate::tool::CanvasToolCtx;
use crate::DocumentSnapshot; use crate::DocumentSnapshot;
use druid::widget::prelude::*; use druid::widget::prelude::*;
use druid::{Color, Data, Env, Event, PointerType}; use druid::{Color, Data, Env, Event};
#[derive(Clone, Data)] #[derive(Clone, Data)]
pub struct CanvasState { pub struct CanvasState {
versioned_canvas: VersionedCanvas, pub versioned_canvas: VersionedCanvas,
tool_ctx: CanvasToolCtx, pub tool_ctx: CanvasToolCtx,
temporary_erasing: bool,
} }
impl CanvasState { impl CanvasState {
pub fn new(tool_ctx: CanvasToolCtx) -> Self {
CanvasState {
versioned_canvas: VersionedCanvas::new(Canvas::new()),
tool_ctx: tool_ctx,
temporary_erasing: true,
}
}
pub fn perform_undo(&mut self) { pub fn perform_undo(&mut self) {
//if !self.is_drawing() { //if !self.is_drawing() {
self.versioned_canvas.undo(); self.versioned_canvas.undo();
@ -58,7 +50,7 @@ impl CanvasState {
canvas_elements: self canvas_elements: self
.versioned_canvas .versioned_canvas
.get() .get()
.elements() .elements
.iter() .iter()
.cloned() .cloned()
.collect(), .collect(),
@ -66,62 +58,22 @@ impl CanvasState {
} }
pub fn set_from_snapshot(&mut self, snapshot: DocumentSnapshot) { pub fn set_from_snapshot(&mut self, snapshot: DocumentSnapshot) {
self.versioned_canvas = VersionedCanvas::new(Canvas::new_with_elements(Vector::from( self.versioned_canvas = VersionedCanvas::new(Canvas {
snapshot.canvas_elements, elements: Vector::from(snapshot.canvas_elements),
))); });
} }
pub fn set_tool_ctx(&mut self, ctx: CanvasToolCtx) { pub fn set_tool_ctx(&mut self, ctx: CanvasToolCtx) {
self.tool_ctx = ctx; self.tool_ctx = ctx;
} }
pub fn get_tool_ctx(&self) -> &CanvasToolCtx {
&self.tool_ctx
}
pub fn get_tool_ctx_mut(&mut self) -> &mut CanvasToolCtx {
&mut self.tool_ctx
}
pub fn handle_event(&mut self, mut ctx: &mut EventCtx, event: &Event, env: &Env) {
self.tool_ctx
.handle_event(ctx, event, &mut self.versioned_canvas, env);
}
} }
pub struct CanvasWidget; pub struct CanvasWidget;
impl Widget<CanvasState> for CanvasWidget { impl Widget<CanvasState> for CanvasWidget {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut CanvasState, env: &Env) { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut CanvasState, env: &Env) {
ctx.request_focus(); data.tool_ctx
let mut toggle_eraser_event = false; .handle_event(ctx, event, &mut data.versioned_canvas, env);
let mut enable_temporary_erasing = false;
match event {
Event::MouseDown(mouse_event) => {
toggle_eraser_event = true;
enable_temporary_erasing = mouse_event.pointer_type == PointerType::Eraser;
}
Event::MouseMove(mouse_event) => {
toggle_eraser_event = true;
enable_temporary_erasing = mouse_event.pointer_type == PointerType::Eraser;
}
Event::MouseUp(mouse_event) => {
toggle_eraser_event = true;
enable_temporary_erasing = mouse_event.pointer_type == PointerType::Eraser;
}
_ => {}
}
// TODO: the first eraser toggle is not handled
if toggle_eraser_event && data.temporary_erasing != enable_temporary_erasing {
if enable_temporary_erasing {
ctx.submit_notification(crate::commands::PUSH_ERASER);
} else {
ctx.submit_notification(crate::commands::POP_ERASER);
}
data.temporary_erasing = enable_temporary_erasing;
} else {
data.handle_event(ctx, event, env);
}
} }
fn lifecycle( fn lifecycle(
@ -176,9 +128,11 @@ impl Widget<CanvasState> for CanvasWidget {
// (ctx.size() returns the size of the layout rect we're painting in) // (ctx.size() returns the size of the layout rect we're painting in)
let size = ctx.size(); let size = ctx.size();
let rect = size.to_rect(); let rect = size.to_rect();
// Note: ctx also has a `clear` method, but that clears the whole context,
// and we only want to clear this widget's area.
ctx.fill(rect, &Color::WHITE); ctx.fill(rect, &Color::WHITE);
for element in data.versioned_canvas.get().elements().iter() {
for element in data.versioned_canvas.get().elements.iter() {
element.draw(ctx); element.draw(ctx);
} }
if data.tool_ctx.needs_repaint() { if data.tool_ctx.needs_repaint() {

View File

@ -1,6 +1,5 @@
pub mod canvas; pub mod canvas;
pub mod tool_icon; pub mod tool_icon;
pub mod tool_ctx;
pub use canvas::*; pub use canvas::*;
pub use tool_icon::*; pub use tool_icon::*;

View File

@ -1,179 +0,0 @@
// Stiletto
// Copyright (C) 2020 Stiletto Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::tool::{CanvasToolParams, CanvasToolType};
use crate::canvas::{Canvas, CanvasElement};
use crate::history::VersionedCanvas;
use druid::kurbo::BezPath;
use druid::{Data, Env, Event, EventCtx, MouseButton, MouseEvent, PaintCtx};
#[derive(Clone, Data)]
pub enum CanvasToolState {
Idle,
DrawingFreehand {
pen_params: CanvasToolParams,
current_path: CanvasElement,
},
Erasing,
}
#[derive(Clone, Data)]
pub struct CanvasToolCtx {
initial_params: CanvasToolParams,
state: CanvasToolState,
}
impl CanvasToolParams {
pub fn tool_type(&self) -> CanvasToolType {
match self {
CanvasToolParams::Pen { .. } => CanvasToolType::Pen,
CanvasToolParams::Eraser => CanvasToolType::Eraser,
}
}
}
fn pressed(mouse_event: &MouseEvent) -> bool {
mouse_event.buttons.contains(MouseButton::Left)
|| mouse_event.button == MouseButton::Left
}
impl CanvasToolCtx {
pub fn new(params: CanvasToolParams) -> Self {
CanvasToolCtx {
initial_params: params,
state: CanvasToolState::Idle,
}
}
pub fn handle_event(
&mut self,
ctx: &EventCtx,
event: &Event,
mut vcanvas: &mut VersionedCanvas,
env: &Env,
) {
match self.initial_params.tool_type() {
CanvasToolType::Pen => self.handle_pen_event(&ctx, &event, &mut vcanvas, &env),
CanvasToolType::Eraser => self.handle_erase_event(&ctx, &event, &mut vcanvas, &env),
}
}
pub fn handle_erase_event(
&mut self,
_ctx: &EventCtx,
event: &Event,
vcanvas: &mut VersionedCanvas,
_env: &Env,
) {
match (&mut self.state, event) {
(CanvasToolState::Idle, Event::MouseDown(mouse_event)) if pressed(mouse_event) => {
self.state = CanvasToolState::Erasing;
}
(CanvasToolState::Erasing, Event::MouseMove(mouse_event)) if pressed(mouse_event) => {
let eraser_rect = druid::Rect::from_center_size(mouse_event.pos, (5.0, 5.0));
let old_elements = vcanvas.get().elements();
let mut new_elements = old_elements.clone();
new_elements.retain(|elem| {
// Check if the element intersects the eraser rect
if elem.bounding_box().intersect(eraser_rect).area() > 0.0 {
if elem.intersects_rect(eraser_rect) {
return false;
}
}
return true;
});
if new_elements.len() != old_elements.len() {
vcanvas.update(|canvas: &mut Canvas| {
*canvas = Canvas::new_with_elements(new_elements);
});
}
}
(CanvasToolState::Erasing, Event::MouseUp(mouse_event)) if pressed(mouse_event) => {
self.state = CanvasToolState::Idle;
}
_ => {}
}
}
pub fn handle_pen_event(
&mut self,
_ctx: &EventCtx,
event: &Event,
vcanvas: &mut VersionedCanvas,
_env: &Env,
) {
match (&mut self.state, event) {
(CanvasToolState::Idle, Event::MouseDown(mouse_event)) if pressed(mouse_event) => {
let mut kurbo_path = BezPath::new();
kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y));
if let CanvasToolParams::Pen { thickness, color } = &self.initial_params {
self.state = CanvasToolState::DrawingFreehand {
pen_params: self.initial_params.clone(),
current_path: CanvasElement::Freehand {
path: crate::canvas::Path { kurbo_path },
thickness: *thickness,
stroke_color: color.clone(),
},
};
}
}
(
CanvasToolState::DrawingFreehand {
ref mut current_path,
..
},
Event::MouseMove(mouse_event),
) => if pressed(mouse_event) {
if let CanvasElement::Freehand { ref mut path, .. } = current_path {
path.kurbo_path
.line_to((mouse_event.pos.x, mouse_event.pos.y));
}
}
(CanvasToolState::DrawingFreehand { .. }, Event::MouseUp(mouse_event)) if pressed(mouse_event) => {
vcanvas.update(move |canvas: &mut Canvas| {
let current_state = std::mem::replace(&mut self.state, CanvasToolState::Idle);
if let CanvasToolState::DrawingFreehand { current_path, .. } = current_state {
if let CanvasElement::Freehand {
mut path,
mut thickness,
stroke_color,
} = current_path
{
canvas.add_element(CanvasElement::Freehand {
path,
thickness,
stroke_color,
});
}
}
});
}
_ => {}
}
}
pub fn needs_repaint(&self) -> bool {
true
}
pub fn paint(&self, ctx: &mut PaintCtx, _env: &Env) {
match &self.state {
CanvasToolState::DrawingFreehand { current_path, .. } => current_path.draw(ctx),
_ => {}
}
}
}