WIP: Eraser implementation #7
|
|
@ -0,0 +1,232 @@
|
||||||
|
use druid::kurbo::BezPath;
|
||||||
|
|
||||||
|
struct Interval {
|
||||||
|
start_pos: f64,
|
||||||
|
end_pos: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interval {
|
||||||
|
fn new(x1: f64, x2: f64) -> Interval {
|
||||||
|
Interval {
|
||||||
|
start_pos: x1.min(x2),
|
||||||
|
end_pos: x1.max(x2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_ordered(start: f64, end: f64) -> Interval {
|
||||||
|
Interval {
|
||||||
|
start_pos: start,
|
||||||
|
end_pos: end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlaps(&self, other: &Self) -> bool {
|
||||||
|
self.start_pos < other.end_pos && other.start_pos < self.end_pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// O(1) complexity
|
||||||
|
// TODO: Implement this in a way that is not so flamboyant
|
||||||
|
// TODO: Implement intersection test for bezier curves
|
||||||
|
fn segment_intersects_rect(segment: &druid::kurbo::PathSeg, rect: druid::Rect) -> bool {
|
||||||
|
match segment {
|
||||||
|
druid::kurbo::PathSeg::Line(line) => {
|
||||||
|
// A Segment intersects the rect
|
||||||
|
// if their projections on the x and y line both overlap
|
||||||
|
let line_x_proj = Interval::new(line.p0.x, line.p1.x);
|
||||||
|
let line_y_proj = Interval::new(line.p0.y, line.p1.y);
|
||||||
|
|
||||||
|
let rect_x_proj = Interval::new_ordered(rect.min_x(), rect.max_x());
|
||||||
|
let rect_y_proj = Interval::new_ordered(rect.min_y(), rect.max_y());
|
||||||
|
|
||||||
|
line_x_proj.overlaps(&rect_x_proj) && line_y_proj.overlaps(&rect_y_proj)
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, druid::Data)]
|
||||||
|
pub struct Path {
|
||||||
|
pub kurbo_path: BezPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Path {
|
||||||
|
fn intersects_rect(&self, rect: druid::Rect) -> bool {
|
||||||
|
// The path intersects the rect if any of its segments does
|
||||||
|
self.kurbo_path.segments().any(|segment| segment_intersects_rect(&segment, rect))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, druid::Data)]
|
||||||
|
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
|
||||||
|
#[derive(Clone, druid::Data)]
|
||||||
|
pub struct Canvas {
|
||||||
|
pub elements: im::Vector<CanvasElement>,
|
||||||
|
pub version_id: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Canvas {
|
||||||
|
pub fn new() -> Canvas {
|
||||||
|
Canvas {
|
||||||
|
elements: im::vector![],
|
||||||
|
version_id: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unchanged(&self, other: &Canvas) -> bool {
|
||||||
|
self.version_id == other.version_id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Completely erase all strokes that intersect the rect
|
||||||
|
pub fn erase_strokes(&mut self, rect: druid::Rect) {
|
||||||
|
// TODO: Improve efficiency
|
||||||
|
|
||||||
|
// Linear search through all elements in the canvas
|
||||||
|
// to find those whose bounding box intersects the eraser rect
|
||||||
|
// (note that this doesn't imply that the element itself intersects the eraser rect,
|
||||||
|
// but it helps filtering out elements that are too far away from the eraser)
|
||||||
|
let mut new_elements = im::Vector::new();
|
||||||
|
|
||||||
|
for elem in self.elements.iter() {
|
||||||
|
// Check if the element intersects the eraser rect
|
||||||
|
if elem.bounding_box().intersect(rect).area() > 0.0 {
|
||||||
|
// Check the element type, we only erase Freehand paths
|
||||||
|
match elem {
|
||||||
|
CanvasElement::Freehand {path, ..} => {
|
||||||
|
// Check whether the path intersects the rect,
|
||||||
|
// if so, remove it.
|
||||||
|
if !path.intersects_rect(rect) {
|
||||||
|
new_elements.push_back(elem.clone());
|
||||||
|
} else {
|
||||||
|
self.version_id += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
new_elements.push_back(elem.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update elements list
|
||||||
|
self.elements = new_elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
// O(n) complexity, where n is the number of segments belonging to paths
|
||||||
|
// that intersect the eraser rectangle
|
||||||
|
// Erase all portions of paths lying in the rect
|
||||||
|
pub fn erase_paths(&mut self, rect: druid::Rect) {
|
||||||
|
// TODO: Improve efficiency
|
||||||
|
|
||||||
|
// Linear search through all elements in the canvas
|
||||||
|
// to find those whose bounding box intersects the eraser rect
|
||||||
|
// (note that this doesn't imply that the element itself intersects the eraser rect,
|
||||||
|
// but it helps filtering out elements that are too far away from the eraser)
|
||||||
|
let mut new_elements = im::Vector::new();
|
||||||
|
|
||||||
|
for elem in self.elements.iter() {
|
||||||
|
// Check if the element intersects the eraser rect
|
||||||
|
if elem.bounding_box().intersect(rect).area() > 0.0 {
|
||||||
|
// Check the element type, we only erase Freehand paths
|
||||||
|
match elem {
|
||||||
|
CanvasElement::Freehand {path, thickness} => {
|
||||||
|
// Remove segments intersecting the eraser
|
||||||
|
// And return the remaining subpaths
|
||||||
|
let (path_changed, new_paths) = Canvas::erase_segments_from_freehand_path(&path, rect);
|
||||||
|
|
||||||
|
if path_changed {
|
||||||
|
// There has been some change, update the canvas
|
||||||
|
self.version_id += 1;
|
||||||
|
for path in new_paths {
|
||||||
|
new_elements.push_back(CanvasElement::Freehand {
|
||||||
|
path: path,
|
||||||
|
thickness: *thickness,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
new_elements.push_back(elem.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update elements list
|
||||||
|
self.elements = new_elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erase segments from a path, and returns the new subpaths that are left
|
||||||
|
fn erase_segments_from_freehand_path(path: &Path, rect: druid::Rect) -> (bool, im::Vector<Path>) {
|
||||||
|
|
||||||
|
// Vector of pieces
|
||||||
|
let mut pieces = im::Vector::new();
|
||||||
|
// Current subpath
|
||||||
|
let mut curr_piece = im::Vector::new();
|
||||||
|
|
||||||
|
// Flag indicating whether the segment has been modified
|
||||||
|
let mut erased = false;
|
||||||
|
|
||||||
|
for segment in path.kurbo_path.segments() {
|
||||||
|
if segment_intersects_rect(&segment, rect) {
|
||||||
|
erased = true;
|
||||||
|
// We found a segment to erase, so split the path here
|
||||||
|
// If the path is not empty, add it to the pieces vector
|
||||||
|
if !curr_piece.is_empty() {
|
||||||
|
pieces.push_back(curr_piece);
|
||||||
|
// Recreate accumulator
|
||||||
|
curr_piece = im::Vector::new();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add segment to curr piece
|
||||||
|
curr_piece.push_back(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We end up with `pieces`, a vector of non empty paths
|
||||||
|
let paths = pieces.iter().map(|segments| Path {
|
||||||
|
kurbo_path: BezPath::from_path_segments(segments.iter().cloned()),
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
(erased, paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_element(&mut self, element: CanvasElement) {
|
||||||
|
self.version_id += 1;
|
||||||
|
self.elements.push_back(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
use crate::canvas::*;
|
||||||
|
use crate::versioned_canvas::VersionedCanvas;
|
||||||
|
|
||||||
|
use druid::kurbo::BezPath;
|
||||||
|
use druid::{Data, Event, Rect};
|
||||||
|
|
||||||
|
// Tools that can be used to interact with the canvas
|
||||||
|
#[derive(Clone, Data)]
|
||||||
|
pub enum CanvasTool {
|
||||||
|
Pen {
|
||||||
|
current_element: Option<CanvasElement>,
|
||||||
|
},
|
||||||
|
Eraser {
|
||||||
|
eraser_rect: Option<Rect>,
|
||||||
|
},
|
||||||
|
|
||||||
|
StrokeEraser {
|
||||||
|
eraser_rect: Option<Rect>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CanvasTool {
|
||||||
|
pub fn new_pen() -> CanvasTool {
|
||||||
|
CanvasTool::Pen {
|
||||||
|
current_element: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_eraser() -> CanvasTool {
|
||||||
|
CanvasTool::Eraser { eraser_rect: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_stroke_eraser() -> CanvasTool {
|
||||||
|
CanvasTool::StrokeEraser { eraser_rect: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_event(&mut self, event: &Event, canvas: &mut VersionedCanvas) {
|
||||||
|
match self {
|
||||||
|
CanvasTool::Pen { current_element } => {
|
||||||
|
CanvasTool::pen_handle_event(current_element, event, canvas)
|
||||||
|
}
|
||||||
|
CanvasTool::Eraser { eraser_rect } => {
|
||||||
|
CanvasTool::eraser_handle_event(eraser_rect, event, canvas, false)
|
||||||
|
}
|
||||||
|
CanvasTool::StrokeEraser { eraser_rect } => {
|
||||||
|
CanvasTool::eraser_handle_event(eraser_rect, event, canvas, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pen_handle_event(
|
||||||
|
current_element: &mut Option<CanvasElement>,
|
||||||
|
event: &Event,
|
||||||
|
canvas: &mut VersionedCanvas,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
Event::MouseDown(mouse_event) => {
|
||||||
|
let mut kurbo_path = BezPath::new();
|
||||||
|
kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y));
|
||||||
|
*current_element = Some(CanvasElement::Freehand {
|
||||||
|
path: Path { kurbo_path },
|
||||||
|
thickness: 2.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Event::MouseMove(mouse_event) => {
|
||||||
|
if current_element.is_some() {
|
||||||
|
if let Some(current_element) = 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 current_element.is_some() {
|
||||||
|
if let Some(current_element) = current_element.take() {
|
||||||
|
canvas.update(move |canvas: &mut Canvas| {
|
||||||
|
canvas.add_element(current_element)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eraser_handle_event(
|
||||||
|
eraser_rect: &mut Option<Rect>,
|
||||||
|
event: &Event,
|
||||||
|
canvas: &mut VersionedCanvas,
|
||||||
|
is_stroke_eraser: bool
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
Event::MouseDown(mouse_event) => {
|
||||||
|
let rect = druid::Rect::from_center_size(mouse_event.pos, druid::Size::new(10.0, 10.0));
|
||||||
|
*eraser_rect = Some(rect);
|
||||||
|
// Create a new undo version each time the mouse is down
|
||||||
|
canvas.update(|canvas: &mut Canvas| {
|
||||||
|
if is_stroke_eraser {
|
||||||
|
canvas.erase_strokes(rect)
|
||||||
|
} else {
|
||||||
|
canvas.erase_paths(rect)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Event::MouseMove(mouse_event) => {
|
||||||
|
if eraser_rect.is_some() {
|
||||||
|
let rect = druid::Rect::from_center_size(
|
||||||
|
mouse_event.pos,
|
||||||
|
druid::Size::new(10.0, 10.0),
|
||||||
|
);
|
||||||
|
*eraser_rect = Some(rect);
|
||||||
|
// We don't want too many levels of undoing
|
||||||
|
// So we make irreversible changes as long as
|
||||||
|
// the mouse is pressed.
|
||||||
|
canvas.irreversible_update(|canvas: &mut Canvas| {
|
||||||
|
if is_stroke_eraser {
|
||||||
|
canvas.erase_strokes(rect)
|
||||||
|
} else {
|
||||||
|
canvas.erase_paths(rect)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::MouseUp(_) => {
|
||||||
|
*eraser_rect = None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/lib.rs
115
src/lib.rs
|
|
@ -1,112 +1,3 @@
|
||||||
#[derive(Clone, druid::Data)]
|
pub mod canvas;
|
||||||
pub struct Path {
|
pub mod canvas_tools;
|
||||||
pub kurbo_path: druid::kurbo::BezPath,
|
pub mod versioned_canvas;
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, druid::Data)]
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use im::Vector;
|
|
||||||
|
|
||||||
// A canvas contains all elements to be drawn
|
|
||||||
pub type Canvas = Vector<CanvasElement>;
|
|
||||||
|
|
||||||
#[derive(Clone, druid::Data)]
|
|
||||||
pub struct VersionedCanvas {
|
|
||||||
// We internally guarantee that this vector
|
|
||||||
// is never empty
|
|
||||||
versions: Vector<Canvas>,
|
|
||||||
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 {
|
|
||||||
let focus = self.versions.focus();
|
|
||||||
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(&Canvas) -> Canvas) {
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
let new_version = update_fn(self.get());
|
|
||||||
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)) {
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
update_fn(self.versions.back_mut().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
167
src/main.rs
167
src/main.rs
|
|
@ -14,35 +14,33 @@
|
||||||
// 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::im::{vector};
|
|
||||||
use druid::kurbo::BezPath;
|
|
||||||
use druid::widget::prelude::*;
|
use druid::widget::prelude::*;
|
||||||
use druid::{AppLauncher, Color, Data, Event, LocalizedString, WindowDesc};
|
use druid::{
|
||||||
|
AppLauncher, Color, Data, Event, Lens, LensExt, LocalizedString, WidgetExt, WindowDesc,
|
||||||
|
};
|
||||||
|
|
||||||
use stiletto::{CanvasElement, VersionedCanvas, Canvas};
|
use stiletto::canvas::Canvas;
|
||||||
|
use stiletto::canvas_tools::CanvasTool;
|
||||||
|
use stiletto::versioned_canvas::VersionedCanvas;
|
||||||
|
|
||||||
#[derive(Clone, Data)]
|
#[derive(Clone, Data, Lens)]
|
||||||
struct CanvasData {
|
struct CanvasData {
|
||||||
|
enrico marked this conversation as resolved
|
|||||||
current_element: Option<CanvasElement>,
|
#[lens(name = "current_tool_lens")]
|
||||||
//elements: Vector<CanvasElement>,
|
current_tool: CanvasTool,
|
||||||
elements: VersionedCanvas,
|
canvas: VersionedCanvas,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CanvasData {
|
impl CanvasData {
|
||||||
fn is_drawing(&self) -> bool {
|
|
||||||
self.current_element.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform_undo(&mut self) {
|
fn perform_undo(&mut self) {
|
||||||
if !self.is_drawing() {
|
self.canvas.undo();
|
||||||
self.elements.undo();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perform_redo(&mut self) {
|
fn perform_redo(&mut self) {
|
||||||
if !self.is_drawing() {
|
self.canvas.redo();
|
||||||
self.elements.redo();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_tool_event(&mut self, event: &Event) {
|
||||||
|
enrico marked this conversation as resolved
enrico
commented
Nice Nice
|
|||||||
|
self.current_tool.handle_event(event, &mut self.canvas);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,41 +48,7 @@ struct CanvasWidget;
|
||||||
|
|
||||||
impl Widget<CanvasData> for CanvasWidget {
|
impl Widget<CanvasData> for CanvasWidget {
|
||||||
fn event(&mut self, _ctx: &mut EventCtx, event: &Event, data: &mut CanvasData, _env: &Env) {
|
fn event(&mut self, _ctx: &mut EventCtx, event: &Event, data: &mut CanvasData, _env: &Env) {
|
||||||
match event {
|
data.handle_tool_event(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: &Canvas| -> Canvas {
|
|
||||||
let mut new_canvas = canvas.clone();
|
|
||||||
new_canvas.push_back(current_element);
|
|
||||||
return new_canvas;
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lifecycle(
|
fn lifecycle(
|
||||||
|
|
@ -103,18 +67,46 @@ impl Widget<CanvasData> for CanvasWidget {
|
||||||
data: &CanvasData,
|
data: &CanvasData,
|
||||||
_env: &Env,
|
_env: &Env,
|
||||||
) {
|
) {
|
||||||
|
// TODO: Polish a bit this logic
|
||||||
|
match (&old_data.current_tool, &data.current_tool) {
|
||||||
|
(
|
||||||
|
CanvasTool::Pen {
|
||||||
|
current_element: old_element,
|
||||||
|
},
|
||||||
|
CanvasTool::Pen {
|
||||||
|
current_element: new_element,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
// the current_element is moved to the elements array, no need to repaint
|
// the current_element is moved to the elements array, no need to repaint
|
||||||
if old_data.is_drawing() && !data.is_drawing() {
|
if old_element.is_some() && !new_element.is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if data.is_drawing() {
|
|
||||||
if let Some(e) = data.current_element.as_ref() {
|
if new_element.is_some() {
|
||||||
|
if let Some(e) = new_element.as_ref() {
|
||||||
ctx.request_paint_rect(e.bounding_box());
|
ctx.request_paint_rect(e.bounding_box());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.request_paint();
|
ctx.request_paint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
(CanvasTool::Eraser{..}, CanvasTool::Eraser{ eraser_rect }) => {
|
||||||
|
if let Some(rect) = eraser_rect {
|
||||||
|
ctx.request_paint_rect(*rect);
|
||||||
|
} else {
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(CanvasTool::StrokeEraser{..}, CanvasTool::StrokeEraser{..}) => {
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
// we just changed the canvas tool, there is no need to repaint
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -143,24 +135,75 @@ impl Widget<CanvasData> for CanvasWidget {
|
||||||
// and we only want to clear this widget's area.
|
// and we only want to clear this widget's area.
|
||||||
ctx.fill(rect, &Color::WHITE);
|
ctx.fill(rect, &Color::WHITE);
|
||||||
|
|
||||||
for element in data.elements.get().iter() {
|
for element in data.canvas.get().elements.iter() {
|
||||||
element.draw(ctx);
|
element.draw(ctx);
|
||||||
}
|
}
|
||||||
if let Some(element) = &data.current_element {
|
|
||||||
|
match &data.current_tool {
|
||||||
|
CanvasTool::Pen { current_element } => {
|
||||||
|
if let Some(element) = ¤t_element {
|
||||||
element.draw(ctx);
|
element.draw(ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enum containing an entry for each canvas tool
|
||||||
|
// it is used to implement the radio group for selecting the tool
|
||||||
|
#[derive(Clone, Data, PartialEq)]
|
||||||
|
enum ToolType {
|
||||||
|
Pen,
|
||||||
|
Eraser,
|
||||||
|
StrokeEraser,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a CanvasTool of a given type
|
||||||
|
fn update_canvas_tool_from_type(tool: &mut CanvasTool, new_type: ToolType) {
|
||||||
|
match new_type {
|
||||||
|
ToolType::Pen => *tool = CanvasTool::new_pen(),
|
||||||
|
ToolType::Eraser => *tool = CanvasTool::new_eraser(),
|
||||||
|
ToolType::StrokeEraser => *tool = CanvasTool::new_stroke_eraser(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a ToolType from a CanvasTool
|
||||||
|
fn canvas_tool_type(tool: &CanvasTool) -> ToolType {
|
||||||
|
match tool {
|
||||||
|
CanvasTool::Pen { .. } => ToolType::Pen,
|
||||||
|
CanvasTool::Eraser { .. } => ToolType::Eraser,
|
||||||
|
CanvasTool::StrokeEraser { .. } => ToolType::StrokeEraser,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_ui() -> impl Widget<CanvasData> {
|
fn build_ui() -> impl Widget<CanvasData> {
|
||||||
use druid::widget::{Align, Button, CrossAxisAlignment, Flex, SizedBox};
|
use druid::widget::{Align, Button, CrossAxisAlignment, Flex, Radio, SizedBox};
|
||||||
|
|
||||||
|
|
||||||
let toolbar = Flex::row()
|
let toolbar = Flex::row()
|
||||||
.cross_axis_alignment(CrossAxisAlignment::Center)
|
.cross_axis_alignment(CrossAxisAlignment::Center)
|
||||||
.with_spacer(30.0)
|
.with_spacer(30.0)
|
||||||
.with_child(
|
.with_child(
|
||||||
Button::new("Undo").on_click(|_ctx: &mut EventCtx, data: &mut CanvasData, _env: &Env| data.perform_undo())
|
Button::new("Undo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_undo()),
|
||||||
)
|
)
|
||||||
.with_child(Button::new("Redo").on_click(|_ctx: &mut EventCtx, data: &mut CanvasData, _env: &Env| data.perform_redo()));
|
.with_child(
|
||||||
|
Button::new("Redo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_redo()),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Radio::new("Pen", ToolType::Pen).lens(CanvasData::current_tool_lens.map(canvas_tool_type, update_canvas_tool_from_type))
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Radio::new("Eraser", ToolType::Eraser).lens(CanvasData::current_tool_lens.map(canvas_tool_type, update_canvas_tool_from_type))
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Radio::new("Stroke Eraser", ToolType::StrokeEraser).lens(CanvasData::current_tool_lens.map(canvas_tool_type, update_canvas_tool_from_type))
|
||||||
|
);
|
||||||
|
// ^ We use a lens to make the radiogroup act only on the tool portion of data.
|
||||||
|
// Actually we must also perform a conversion, between ToolType and CanvasTool,
|
||||||
|
// that's why we perform a map on the lens.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.cross_axis_alignment(CrossAxisAlignment::Center)
|
.cross_axis_alignment(CrossAxisAlignment::Center)
|
||||||
|
|
@ -176,8 +219,8 @@ pub fn main() {
|
||||||
LocalizedString::new("custom-widget-demo-window-title").with_placeholder("Stiletto"),
|
LocalizedString::new("custom-widget-demo-window-title").with_placeholder("Stiletto"),
|
||||||
);
|
);
|
||||||
let canvas_data = CanvasData {
|
let canvas_data = CanvasData {
|
||||||
current_element: None,
|
current_tool: CanvasTool::new_pen(),
|
||||||
elements: VersionedCanvas::new(vector![]),
|
canvas: VersionedCanvas::new(Canvas::new()),
|
||||||
};
|
};
|
||||||
AppLauncher::with_window(window)
|
AppLauncher::with_window(window)
|
||||||
.use_simple_logger()
|
.use_simple_logger()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
use crate::canvas::Canvas;
|
||||||
|
|
||||||
|
#[derive(Clone, druid::Data)]
|
||||||
|
pub struct VersionedCanvas {
|
||||||
|
// We internally guarantee that this vector
|
||||||
|
// is never empty
|
||||||
|
versions: im::Vector<Canvas>,
|
||||||
|
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 version(&self) -> usize {
|
||||||
|
self.curr_version
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Right now the update function internally checks
|
||||||
|
// whether the canvas has been changed by leveraging the
|
||||||
|
// Copy on Write semantics of im::Vector.
|
||||||
|
// Is this a good solution? Does this work correctly? THINK ABOUT THIS
|
||||||
|
pub fn update(&mut self, update_fn: impl FnOnce(&mut Canvas)) {
|
||||||
|
let mut new_version = self.get().clone();
|
||||||
|
update_fn(&mut new_version);
|
||||||
|
|
||||||
|
// Only push new version if there has been an actual change in the vector
|
||||||
|
if !new_version.unchanged(&self.get()) {
|
||||||
|
// 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)) {
|
||||||
|
let old_version = self.get().version_id;
|
||||||
|
|
||||||
|
update_fn(self.versions.back_mut().unwrap());
|
||||||
|
|
||||||
|
// Check whether there has been a new change
|
||||||
|
if old_version != self.get().version_id {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
When does
the current path being drawnmake sense?I felt a little uncomfortable keeping this in CanvasData, because there is no current path being drawn when we erase. Probably with the state machine we can achieve a cleaner vision, though.
It still fits the CanvasData, because it is a piece of data associated to the Canvas (indeed, the currently drawn item). When there is no currently drawn item, is set to None; for the time being, current_element != None is the result of is_drawing()