From 7925cfb2d8d0acb966f00232f18c773003f0af77 Mon Sep 17 00:00:00 2001 From: Enrico Lumetti Date: Sat, 21 Nov 2020 21:05:51 +0100 Subject: [PATCH] Split stiletto libraries in modules --- src/canvas.rs | 115 +++++++++++++++++++++++++++++++ src/history.rs | 92 +++++++++++++++++++++++++ src/lib.rs | 179 ++---------------------------------------------- src/main.rs | 182 +++++++------------------------------------------ src/widget.rs | 163 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 398 insertions(+), 333 deletions(-) create mode 100644 src/canvas.rs create mode 100644 src/history.rs create mode 100644 src/widget.rs diff --git a/src/canvas.rs b/src/canvas.rs new file mode 100644 index 0000000..4284f10 --- /dev/null +++ b/src/canvas.rs @@ -0,0 +1,115 @@ +// 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 . + +use im::Vector; + +use serde::de::{Deserializer, SeqAccess, Visitor}; +use serde::ser::Serializer; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, druid::Data)] +pub struct Path { + pub kurbo_path: druid::kurbo::BezPath, +} + +pub type Canvas = Vector; + +#[derive(Debug, Clone, druid::Data, Serialize, Deserialize)] +pub enum CanvasElement { + Freehand { path: Path, thickness: f64 }, +} + +impl CanvasElement { + pub fn bounding_box(&self) -> druid::Rect { + match self { + CanvasElement::Freehand { path, thickness } => { + use druid::kurbo::Shape; + path.kurbo_path + .bounding_box() + .inflate(*thickness, *thickness) + } + } + } + pub fn draw(&self, ctx: &mut druid::PaintCtx) { + match self { + CanvasElement::Freehand { path, thickness } => { + use druid::RenderContext; + let stroke_color = druid::Color::rgb8(0, 128, 0); + ctx.stroke(&path.kurbo_path, &stroke_color, *thickness); + } + } + } + + pub fn get_path_mut(&mut self) -> Option<&mut Path> { + match self { + CanvasElement::Freehand { path, .. } => Some(path), + } + } +} + +impl<'de> Deserialize<'de> for Path { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_seq(PathDeserializer) + } +} + +impl Serialize for Path { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + use druid::kurbo::PathEl; + + serializer.collect_seq(self.kurbo_path.iter().filter_map(|path_el| match path_el { + PathEl::MoveTo(pt) => Some(Into::<(f64, f64)>::into(pt)), + PathEl::LineTo(pt) => Some(Into::<(f64, f64)>::into(pt)), + _ => None, + })) + } +} + +struct PathDeserializer; + +impl<'de> Visitor<'de> for PathDeserializer { + type Value = Path; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "A sequence of 2D points") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + use druid::kurbo::BezPath; + + let mut kurbo_path = BezPath::new(); + let mut first_element = true; + while let Some(point) = seq.next_element::<(f64, f64)>()? { + if first_element { + kurbo_path.move_to(point); + first_element = false; + } else { + kurbo_path.line_to(point); + } + } + + Ok(Path { kurbo_path }) + } +} diff --git a/src/history.rs b/src/history.rs new file mode 100644 index 0000000..44ab433 --- /dev/null +++ b/src/history.rs @@ -0,0 +1,92 @@ +// 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 . + +use super::canvas::Canvas; +use im::Vector; + +#[derive(Clone, druid::Data)] +pub struct VersionedCanvas { + // We internally guarantee that this vector + // is never empty + versions: Vector, + curr_version: usize, +} + +impl VersionedCanvas { + pub fn new(canvas: Canvas) -> VersionedCanvas { + VersionedCanvas { + versions: im::vector![canvas], + curr_version: 0, + } + } + + // Get current canvas version + pub fn get(&self) -> &Canvas { + self.versions.get(self.curr_version).unwrap() + } + + fn has_newer_versions(&self) -> bool { + self.curr_version + 1 < self.versions.len() + } + + fn has_older_versions(&self) -> bool { + self.curr_version > 0 + } + + pub fn undo(&mut self) { + if self.has_older_versions() { + self.curr_version = self.curr_version - 1; + } + } + + pub fn redo(&mut self) { + if self.has_newer_versions() { + self.curr_version = self.curr_version + 1; + } + } + + pub fn update(&mut self, update_fn: impl FnOnce(&mut Canvas)) { + // Make a new copy of the current canvas version, + // so that we can safely modify it without losing + // the previous canvas version + let mut new_version = self.get().clone(); + update_fn(&mut new_version); + + // This is a linear history, + // so we first check if there are newer versions, if so + // this means we are in the past, so a change in the past destroys the future + // and creates a new future. + if self.has_newer_versions() { + self.versions = self.versions.take(self.curr_version + 1); + } + self.versions.push_back(new_version); + self.curr_version = self.curr_version + 1; + } + + // Do inplace update, which will be irreversible + pub fn irreversible_update(&mut self, update_fn: impl FnOnce(&mut Canvas)) { + // Do the update directly on the current canvas version + update_fn(self.versions.back_mut().unwrap()); + + // This is a linear history, + // so we first check if there are newer versions, if so + // this means we are in the past, so a change in the past destroys the future + // and creates a new future. + if self.has_newer_versions() { + self.versions = self.versions.take(self.curr_version + 1); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 74401b4..a9e355d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,185 +12,16 @@ // 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 .use serde::{Serialize, Deserialize}; +// along with this program. If not, see . -use im::Vector; - -use serde::de::{Deserializer, SeqAccess, Visitor}; -use serde::ser::Serializer; use serde::{Deserialize, Serialize}; use std::vec::Vec; -#[derive(Debug, Clone, druid::Data)] -pub struct Path { - pub kurbo_path: druid::kurbo::BezPath, -} - -#[derive(Debug, Clone, druid::Data, Serialize, Deserialize)] -pub enum CanvasElement { - Freehand { path: Path, thickness: f64 }, -} - -impl CanvasElement { - pub fn bounding_box(&self) -> druid::Rect { - match self { - CanvasElement::Freehand { path, thickness } => { - use druid::kurbo::Shape; - path.kurbo_path - .bounding_box() - .inflate(*thickness, *thickness) - } - } - } - pub fn draw(&self, ctx: &mut druid::PaintCtx) { - match self { - CanvasElement::Freehand { path, thickness } => { - use druid::RenderContext; - let stroke_color = druid::Color::rgb8(0, 128, 0); - ctx.stroke(&path.kurbo_path, &stroke_color, *thickness); - } - } - } - - pub fn get_path_mut(&mut self) -> Option<&mut Path> { - match self { - CanvasElement::Freehand { path, .. } => Some(path), - } - } -} - -// A canvas contains all elements to be drawn -pub type Canvas = Vector; - -#[derive(Clone, druid::Data)] -pub struct VersionedCanvas { - // We internally guarantee that this vector - // is never empty - versions: Vector, - curr_version: usize, -} - -impl VersionedCanvas { - pub fn new(canvas: Canvas) -> VersionedCanvas { - VersionedCanvas { - versions: im::vector![canvas], - curr_version: 0, - } - } - - // Get current canvas version - pub fn get(&self) -> &Canvas { - self.versions.get(self.curr_version).unwrap() - } - - fn has_newer_versions(&self) -> bool { - self.curr_version + 1 < self.versions.len() - } - - fn has_older_versions(&self) -> bool { - self.curr_version > 0 - } - - pub fn undo(&mut self) { - if self.has_older_versions() { - self.curr_version = self.curr_version - 1; - } - } - - pub fn redo(&mut self) { - if self.has_newer_versions() { - self.curr_version = self.curr_version + 1; - } - } - - pub fn update(&mut self, update_fn: impl FnOnce(&mut Canvas)) { - // Make a new copy of the current canvas version, - // so that we can safely modify it without losing - // the previous canvas version - let mut new_version = self.get().clone(); - update_fn(&mut new_version); - - // This is a linear history, - // so we first check if there are newer versions, if so - // this means we are in the past, so a change in the past destroys the future - // and creates a new future. - if self.has_newer_versions() { - self.versions = self.versions.take(self.curr_version + 1); - } - self.versions.push_back(new_version); - self.curr_version = self.curr_version + 1; - } - - // Do inplace update, which will be irreversible - pub fn irreversible_update(&mut self, update_fn: impl FnOnce(&mut Canvas)) { - // Do the update directly on the current canvas version - update_fn(self.versions.back_mut().unwrap()); - - // This is a linear history, - // so we first check if there are newer versions, if so - // this means we are in the past, so a change in the past destroys the future - // and creates a new future. - if self.has_newer_versions() { - self.versions = self.versions.take(self.curr_version + 1); - } - } -} - -struct PathDeserializer; - -impl<'de> Visitor<'de> for PathDeserializer { - type Value = Path; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "A sequence of 2D points") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - use druid::kurbo::BezPath; - - let mut kurbo_path = BezPath::new(); - let mut first_element = true; - while let Some(point) = seq.next_element::<(f64, f64)>()? { - if first_element { - kurbo_path.move_to(point); - first_element = false; - } else { - kurbo_path.line_to(point); - } - } - - Ok(Path { kurbo_path }) - } -} - -impl<'de> Deserialize<'de> for Path { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_seq(PathDeserializer) - } -} - -impl Serialize for Path { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - use druid::kurbo::PathEl; - - serializer.collect_seq(self.kurbo_path.iter().filter_map(|path_el| match path_el { - PathEl::MoveTo(pt) => Some(Into::<(f64, f64)>::into(pt)), - PathEl::LineTo(pt) => Some(Into::<(f64, f64)>::into(pt)), - _ => None, - })) - } -} +pub mod canvas; +pub mod history; +pub mod widget; #[derive(Serialize, Deserialize, Debug)] pub struct DocumentSnapshot { - pub canvas_elements: Vec, + pub canvas_elements: Vec, } diff --git a/src/main.rs b/src/main.rs index 89d7424..a211f64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,157 +17,36 @@ use log::{info, warn}; use druid::commands; -use druid::im::{vector, Vector}; -use druid::kurbo::BezPath; +use druid::im::vector; use druid::widget::prelude::*; +use druid::widget::{Align, Button, CrossAxisAlignment, Flex, SizedBox}; use druid::{ - AppDelegate, AppLauncher, Color, Command, Data, DelegateCtx, Env, Event, FileDialogOptions, - FileSpec, LocalizedString, Target, WindowDesc, + AppDelegate, AppLauncher, Command, DelegateCtx, Env, FileDialogOptions, FileSpec, + LocalizedString, Target, WindowDesc, }; -use stiletto::{Canvas, CanvasElement, DocumentSnapshot, VersionedCanvas}; +use stiletto::history::VersionedCanvas; +use stiletto::widget::{CanvasData, CanvasWidget}; +use stiletto::DocumentSnapshot; -struct Delegate; - -#[derive(Clone, Data)] -struct CanvasData { - current_element: Option, - elements: VersionedCanvas, -} - -impl CanvasData { - fn is_drawing(&self) -> bool { - self.current_element.is_some() - } - - fn perform_undo(&mut self) { - if !self.is_drawing() { - self.elements.undo(); - } - } - - fn perform_redo(&mut self) { - if !self.is_drawing() { - self.elements.redo(); - } - } - - fn get_document_snapshot(&self) -> DocumentSnapshot { - DocumentSnapshot { - canvas_elements: self.elements.get().iter().cloned().collect(), - } - } - - fn set_from_snapshot(&mut self, snapshot: DocumentSnapshot) { - self.current_element = None; - self.elements = VersionedCanvas::new(Vector::from(snapshot.canvas_elements)); - } -} - -struct CanvasWidget; - -impl Widget for CanvasWidget { - fn event(&mut self, _ctx: &mut EventCtx, event: &Event, data: &mut CanvasData, _env: &Env) { - match event { - Event::MouseDown(mouse_event) => { - let mut kurbo_path = BezPath::new(); - kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y)); - data.current_element = Some(CanvasElement::Freehand { - path: stiletto::Path { kurbo_path }, - thickness: 2.0, - }); - } - Event::MouseMove(mouse_event) => { - if data.is_drawing() { - if let Some(current_element) = data.current_element.as_mut() { - current_element - .get_path_mut() - .unwrap() - .kurbo_path - .line_to((mouse_event.pos.x, mouse_event.pos.y)); - } - } - } - Event::MouseUp(_) => { - if data.is_drawing() { - if let Some(current_element) = data.current_element.take() { - data.elements.update(move |canvas: &mut Canvas| { - canvas.push_back(current_element); - }); - } - } - } - _ => {} - } - } - - fn lifecycle( - &mut self, - _ctx: &mut LifeCycleCtx, - _event: &LifeCycle, - _data: &CanvasData, - _env: &Env, - ) { - } - - fn update( - &mut self, - ctx: &mut UpdateCtx, - old_data: &CanvasData, - data: &CanvasData, - _env: &Env, - ) { - // the current_element is moved to the elements array, no need to repaint - if old_data.is_drawing() && !data.is_drawing() { - return; - } - if data.is_drawing() { - if let Some(e) = data.current_element.as_ref() { - ctx.request_paint_rect(e.bounding_box()); - } - } else { - ctx.request_paint(); - } - } - - fn layout( - &mut self, - _layout_ctx: &mut LayoutCtx, - bc: &BoxConstraints, - _data: &CanvasData, - _env: &Env, - ) -> Size { - // BoxConstraints are passed by the parent widget. - // This method can return any Size within those constraints: - // bc.constrain(my_size) - // - // To check if a dimension is infinite or not (e.g. scrolling): - // bc.is_width_bounded() / bc.is_height_bounded() - bc.max() - } - - // The paint method gets called last, after an event flow. - // It goes event -> update -> layout -> paint, and each method can influence the next. - // Basically, anything that changes the appearance of a widget causes a paint. - fn paint(&mut self, ctx: &mut PaintCtx, data: &CanvasData, _env: &Env) { - // (ctx.size() returns the size of the layout rect we're painting in) - let size = ctx.size(); - 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); - - for element in data.elements.get().iter() { - element.draw(ctx); - } - if let Some(element) = &data.current_element { - element.draw(ctx); - } - } +pub fn main() { + let window = WindowDesc::new(build_ui) + .window_size((1024.0, 1400.0)) + .title( + LocalizedString::new("custom-widget-demo-window-title").with_placeholder("Stiletto"), + ); + let canvas_data = CanvasData { + current_element: None, + elements: VersionedCanvas::new(vector![]), + }; + AppLauncher::with_window(window) + .use_simple_logger() + .delegate(Delegate) + .launch(canvas_data) + .expect("launch failed"); } fn build_ui() -> impl Widget { - use druid::widget::{Align, Button, CrossAxisAlignment, Flex, SizedBox}; let history_buttons = Flex::row() .cross_axis_alignment(CrossAxisAlignment::Center) @@ -219,22 +98,7 @@ fn build_ui() -> impl Widget { .with_flex_child(CanvasWidget {}, 1.0) } -pub fn main() { - let window = WindowDesc::new(build_ui) - .window_size((1024.0, 1400.0)) - .title( - LocalizedString::new("custom-widget-demo-window-title").with_placeholder("Stiletto"), - ); - let canvas_data = CanvasData { - current_element: None, - elements: VersionedCanvas::new(vector![]), - }; - AppLauncher::with_window(window) - .use_simple_logger() - .delegate(Delegate) - .launch(canvas_data) - .expect("launch failed"); -} +struct Delegate; impl AppDelegate for Delegate { fn command( diff --git a/src/widget.rs b/src/widget.rs new file mode 100644 index 0000000..90bf939 --- /dev/null +++ b/src/widget.rs @@ -0,0 +1,163 @@ +// 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 . + +use im::Vector; + +use super::canvas; +use super::canvas::{Canvas, CanvasElement}; +use super::history::VersionedCanvas; +use super::DocumentSnapshot; + +use druid::kurbo::BezPath; +use druid::widget::prelude::*; +use druid::{Color, Data, Env, Event}; + +#[derive(Clone, Data)] +pub struct CanvasData { + pub current_element: Option, + pub elements: VersionedCanvas, +} + +impl CanvasData { + pub fn is_drawing(&self) -> bool { + self.current_element.is_some() + } + + pub fn perform_undo(&mut self) { + if !self.is_drawing() { + self.elements.undo(); + } + } + + pub fn perform_redo(&mut self) { + if !self.is_drawing() { + self.elements.redo(); + } + } + + pub fn get_document_snapshot(&self) -> DocumentSnapshot { + DocumentSnapshot { + canvas_elements: self.elements.get().iter().cloned().collect(), + } + } + + pub fn set_from_snapshot(&mut self, snapshot: DocumentSnapshot) { + self.current_element = None; + self.elements = VersionedCanvas::new(Vector::from(snapshot.canvas_elements)); + } +} + +pub struct CanvasWidget; + +impl Widget for CanvasWidget { + fn event(&mut self, _ctx: &mut EventCtx, event: &Event, data: &mut CanvasData, _env: &Env) { + match event { + Event::MouseDown(mouse_event) => { + let mut kurbo_path = BezPath::new(); + kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y)); + data.current_element = Some(CanvasElement::Freehand { + path: canvas::Path { kurbo_path }, + thickness: 2.0, + }); + } + Event::MouseMove(mouse_event) => { + if data.is_drawing() { + if let Some(current_element) = data.current_element.as_mut() { + current_element + .get_path_mut() + .unwrap() + .kurbo_path + .line_to((mouse_event.pos.x, mouse_event.pos.y)); + } + } + } + Event::MouseUp(_) => { + if data.is_drawing() { + if let Some(current_element) = data.current_element.take() { + data.elements.update(move |canvas: &mut Canvas| { + canvas.push_back(current_element); + }); + } + } + } + _ => {} + } + } + + fn lifecycle( + &mut self, + _ctx: &mut LifeCycleCtx, + _event: &LifeCycle, + _data: &CanvasData, + _env: &Env, + ) { + } + + fn update( + &mut self, + ctx: &mut UpdateCtx, + old_data: &CanvasData, + data: &CanvasData, + _env: &Env, + ) { + // the current_element is moved to the elements array, no need to repaint + if old_data.is_drawing() && !data.is_drawing() { + return; + } + if data.is_drawing() { + if let Some(e) = data.current_element.as_ref() { + ctx.request_paint_rect(e.bounding_box()); + } + } else { + ctx.request_paint(); + } + } + + fn layout( + &mut self, + _layout_ctx: &mut LayoutCtx, + bc: &BoxConstraints, + _data: &CanvasData, + _env: &Env, + ) -> Size { + // BoxConstraints are passed by the parent widget. + // This method can return any Size within those constraints: + // bc.constrain(my_size) + // + // To check if a dimension is infinite or not (e.g. scrolling): + // bc.is_width_bounded() / bc.is_height_bounded() + bc.max() + } + + // The paint method gets called last, after an event flow. + // It goes event -> update -> layout -> paint, and each method can influence the next. + // Basically, anything that changes the appearance of a widget causes a paint. + fn paint(&mut self, ctx: &mut PaintCtx, data: &CanvasData, _env: &Env) { + // (ctx.size() returns the size of the layout rect we're painting in) + let size = ctx.size(); + 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); + + for element in data.elements.get().iter() { + element.draw(ctx); + } + if let Some(element) = &data.current_element { + element.draw(ctx); + } + } +}