WIP: Eraser implementation #7
|
|
@ -0,0 +1,88 @@
|
|||
#[derive(Clone, druid::Data)]
|
||||
pub struct Path {
|
||||
pub kurbo_path: druid::kurbo::BezPath,
|
||||
}
|
||||
|
||||
#[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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// O(1) complexity
|
||||
fn segment_intersects_rect(segment: &druid::kurbo::PathSeg, rect: druid::Rect) -> bool {
|
||||
match segment {
|
||||
druid::kurbo::PathSeg::Line(line) => rect.contains(line.p0) || rect.contains(line.p1),
|
||||
|
||||
druid::kurbo::PathSeg::Quad(quad) => {
|
||||
rect.contains(quad.p0) || rect.contains(quad.p1) || rect.contains(quad.p2)
|
||||
}
|
||||
|
||||
druid::kurbo::PathSeg::Cubic(cubic) => {
|
||||
rect.contains(cubic.p0) || rect.contains(cubic.p1) || rect.contains(cubic.p2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A canvas contains all elements to be drawn
|
||||
#[derive(Clone, druid::Data)]
|
||||
pub struct Canvas {
|
||||
pub elements: im::Vector<CanvasElement>,
|
||||
}
|
||||
|
||||
impl Canvas {
|
||||
pub fn new() -> Canvas {
|
||||
Canvas {
|
||||
elements: im::vector![],
|
||||
}
|
||||
}
|
||||
// O(n) complexity, where n is the number of segments belonging to paths
|
||||
// that intersect the eraser rectangle
|
||||
pub fn erase(&mut self, eraser_rect: druid::Rect) {
|
||||
for elem in self.elements.iter_mut() {
|
||||
if elem.bounding_box().intersect(eraser_rect).area() > 0.0 {
|
||||
match elem {
|
||||
CanvasElement::Freehand { path, thickness: _ } => {
|
||||
// Remove segments intersecting the eraser
|
||||
let new_segments = (*path)
|
||||
.kurbo_path
|
||||
.segments()
|
||||
.filter(|path| !segment_intersects_rect(path, eraser_rect));
|
||||
path.kurbo_path = druid::kurbo::BezPath::from_path_segments(new_segments);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_back(&mut self, element: CanvasElement) {
|
||||
self.elements.push_back(element);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
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>,
|
||||
},
|
||||
}
|
||||
|
||||
impl CanvasTool {
|
||||
pub fn new_pen() -> CanvasTool {
|
||||
CanvasTool::Pen {
|
||||
current_element: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_eraser() -> CanvasTool {
|
||||
CanvasTool::Eraser { 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: &Canvas| -> Canvas {
|
||||
let mut new_canvas = canvas.clone();
|
||||
new_canvas.push_back(current_element);
|
||||
return new_canvas;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn eraser_handle_event(
|
||||
eraser_rect: &mut Option<Rect>,
|
||||
event: &Event,
|
||||
canvas: &mut VersionedCanvas,
|
||||
) {
|
||||
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: &Canvas| -> Canvas {
|
||||
let mut new_canvas = canvas.clone();
|
||||
new_canvas.erase(rect);
|
||||
return new_canvas;
|
||||
});
|
||||
}
|
||||
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| {
|
||||
canvas.erase(rect);
|
||||
});
|
||||
}
|
||||
}
|
||||
Event::MouseUp(_) => {
|
||||
*eraser_rect = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
163
src/lib.rs
163
src/lib.rs
|
|
@ -1,160 +1,3 @@
|
|||
#[derive(Clone, druid::Data)]
|
||||
pub struct Path {
|
||||
pub kurbo_path: druid::kurbo::BezPath,
|
||||
}
|
||||
|
||||
#[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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// O(1) complexity
|
||||
fn segment_intersects_rect(segment: &druid::kurbo::PathSeg, rect: druid::Rect) -> bool {
|
||||
|
||||
match segment {
|
||||
druid::kurbo::PathSeg::Line(line) => rect.contains(line.p0)
|
||||
|| rect.contains(line.p1),
|
||||
|
||||
druid::kurbo::PathSeg::Quad(quad) => rect.contains(quad.p0)
|
||||
|| rect.contains(quad.p1)
|
||||
|| rect.contains(quad.p2),
|
||||
|
||||
druid::kurbo::PathSeg::Cubic(cubic) => rect.contains(cubic.p0)
|
||||
|| rect.contains(cubic.p1)
|
||||
|| rect.contains(cubic.p2),
|
||||
}
|
||||
}
|
||||
|
||||
// A canvas contains all elements to be drawn
|
||||
#[derive(Clone, druid::Data)]
|
||||
pub struct Canvas {
|
||||
pub elements: im::Vector<CanvasElement>,
|
||||
}
|
||||
|
||||
impl Canvas {
|
||||
pub fn new() -> Canvas {
|
||||
Canvas {
|
||||
elements: im::vector![],
|
||||
}
|
||||
}
|
||||
// O(n) complexity, where n is the number of segments belonging to paths
|
||||
// that intersect the eraser rectangle
|
||||
pub fn erase(&mut self, eraser_rect: druid::Rect) {
|
||||
for elem in self.elements.iter_mut() {
|
||||
if elem.bounding_box().intersect(eraser_rect).area() > 0.0 {
|
||||
match elem {
|
||||
CanvasElement::Freehand{path, thickness: _} => {
|
||||
// Remove segments intersecting the eraser
|
||||
let new_segments = (*path).kurbo_path.segments().filter(|path| ! segment_intersects_rect(path, eraser_rect));
|
||||
path.kurbo_path = druid::kurbo::BezPath::from_path_segments(new_segments);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_back(&mut self, element: CanvasElement) {
|
||||
self.elements.push_back(element);
|
||||
}
|
||||
}
|
||||
|
||||
#[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;
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
pub mod canvas;
|
||||
pub mod canvas_tools;
|
||||
pub mod versioned_canvas;
|
||||
|
|
|
|||
199
src/main.rs
199
src/main.rs
|
|
@ -14,74 +14,23 @@
|
|||
// 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 druid::kurbo::{BezPath, Rect};
|
||||
use druid::widget::prelude::*;
|
||||
use druid::{AppLauncher, Color, Data, Lens, Event, LocalizedString, WidgetExt, 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, PartialEq)]
|
||||
enum CanvasToolType {
|
||||
Pen,
|
||||
Eraser
|
||||
}
|
||||
|
||||
// Tools that can be used to interact with the canvas
|
||||
#[derive(Clone, Data)]
|
||||
enum CanvasTool {
|
||||
Pen { current_element: Option<CanvasElement> },
|
||||
Eraser { eraser_rect: Option<Rect> },
|
||||
}
|
||||
|
||||
impl CanvasTool {
|
||||
fn new_pen() -> CanvasTool {
|
||||
CanvasTool::Pen { current_element: None }
|
||||
}
|
||||
fn new_eraser() -> CanvasTool {
|
||||
CanvasTool::Eraser { eraser_rect: None }
|
||||
}
|
||||
|
||||
fn tool_type(&self) -> CanvasToolType {
|
||||
match self {
|
||||
CanvasTool::Pen { current_element: _ } => CanvasToolType::Pen,
|
||||
CanvasTool::Eraser { eraser_rect: _ } => CanvasToolType::Eraser,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CanvasToolLens;
|
||||
|
||||
impl Lens<CanvasData, CanvasToolType> for CanvasToolLens {
|
||||
fn with<R, F: FnOnce(&CanvasToolType) -> R>(&self, data: &CanvasData, f: F) -> R {
|
||||
f(&data.current_tool.tool_type())
|
||||
}
|
||||
|
||||
fn with_mut<R, F: FnOnce(&mut CanvasToolType) -> R>(&self, data: &mut CanvasData, f: F) -> R {
|
||||
let mut curr_type = data.current_tool.tool_type();
|
||||
let result = f(&mut curr_type);
|
||||
|
||||
match curr_type {
|
||||
CanvasToolType::Pen => data.current_tool = CanvasTool::new_pen(),
|
||||
CanvasToolType::Eraser => data.current_tool = CanvasTool::new_eraser(),
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Data)]
|
||||
#[derive(Clone, Data, Lens)]
|
||||
struct CanvasData {
|
||||
|
enrico marked this conversation as resolved
|
||||
#[lens(name = "current_tool_lens")]
|
||||
current_tool: CanvasTool,
|
||||
elements: VersionedCanvas,
|
||||
}
|
||||
|
||||
impl CanvasData {
|
||||
/*fn is_drawing(&self) -> bool {
|
||||
self.current_element.is_some()
|
||||
}*/
|
||||
|
||||
const CURRENT_TOOL_LENS: CanvasToolLens = CanvasToolLens;
|
||||
|
||||
fn perform_undo(&mut self) {
|
||||
self.elements.undo();
|
||||
}
|
||||
|
|
@ -91,85 +40,15 @@ impl CanvasData {
|
|||
}
|
||||
|
||||
fn handle_tool_event(&mut self, event: &Event) {
|
||||
|
enrico marked this conversation as resolved
enrico
commented
Nice Nice
|
||||
match &mut self.current_tool {
|
||||
CanvasTool::Pen { current_element } => {
|
||||
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: stiletto::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() {
|
||||
|
||||
self.elements.update(move |canvas: &Canvas| -> Canvas {
|
||||
let mut new_canvas = canvas.clone();
|
||||
new_canvas.push_back(current_element);
|
||||
return new_canvas;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
CanvasTool::Eraser { eraser_rect } => {
|
||||
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
|
||||
self.elements.update(|canvas: &Canvas| -> Canvas {
|
||||
let mut new_canvas = canvas.clone();
|
||||
new_canvas.erase(rect);
|
||||
return new_canvas;
|
||||
});
|
||||
}
|
||||
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.
|
||||
self.elements.irreversible_update(|canvas: &mut Canvas| {
|
||||
canvas.erase(rect);
|
||||
});
|
||||
}
|
||||
}
|
||||
Event::MouseUp(_) => {
|
||||
*eraser_rect = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
self.current_tool.handle_event(event, &mut self.elements);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct CanvasWidget;
|
||||
|
||||
impl Widget<CanvasData> for CanvasWidget {
|
||||
fn event(&mut self, _ctx: &mut EventCtx, event: &Event, data: &mut CanvasData, _env: &Env) {
|
||||
data.handle_tool_event(event);
|
||||
|
||||
}
|
||||
|
||||
fn lifecycle(
|
||||
|
|
@ -188,12 +67,19 @@ impl Widget<CanvasData> for CanvasWidget {
|
|||
data: &CanvasData,
|
||||
_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 }) => {
|
||||
(
|
||||
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
|
||||
if old_element.is_some() && !new_element.is_some() {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if new_element.is_some() {
|
||||
|
|
@ -204,7 +90,7 @@ impl Widget<CanvasData> for CanvasWidget {
|
|||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
(CanvasTool::Eraser{ eraser_rect: _ }, CanvasTool::Eraser{ eraser_rect }) => {
|
||||
(CanvasTool::Eraser { eraser_rect: _ }, CanvasTool::Eraser { eraser_rect }) => {
|
||||
// We just stopped erasing, no need to repaint
|
||||
if let Some(rect) = eraser_rect {
|
||||
ctx.request_paint_rect(*rect);
|
||||
|
|
@ -261,18 +147,49 @@ impl Widget<CanvasData> for CanvasWidget {
|
|||
}
|
||||
}
|
||||
|
||||
fn build_ui() -> impl Widget<CanvasData> {
|
||||
use druid::widget::{Align, Button, RadioGroup, CrossAxisAlignment, Flex, SizedBox};
|
||||
// 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,
|
||||
}
|
||||
|
||||
let radio_group = RadioGroup::new(vec![("Pen", CanvasToolType::Pen), ("Eraser", CanvasToolType::Eraser)]);
|
||||
|
||||
// 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(),
|
||||
}
|
||||
}
|
||||
|
||||
// Get a ToolType from a CanvasTool
|
||||
fn canvas_tool_type(tool: &CanvasTool) -> ToolType {
|
||||
match tool {
|
||||
CanvasTool::Pen { .. } => ToolType::Pen,
|
||||
CanvasTool::Eraser { .. } => ToolType::Eraser,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_ui() -> impl Widget<CanvasData> {
|
||||
use druid::widget::{Align, Button, CrossAxisAlignment, Flex, RadioGroup, SizedBox};
|
||||
|
||||
let radio_group = RadioGroup::new(vec![("Pen", ToolType::Pen), ("Eraser", ToolType::Eraser)])
|
||||
.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.
|
||||
|
||||
let toolbar = Flex::row()
|
||||
.cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_spacer(30.0)
|
||||
.with_child(Button::new("Undo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_undo()))
|
||||
.with_child(Button::new("Redo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_redo()))
|
||||
.with_child(radio_group.lens(CanvasData::CURRENT_TOOL_LENS));
|
||||
.with_child(
|
||||
Button::new("Undo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_undo()),
|
||||
)
|
||||
.with_child(
|
||||
Button::new("Redo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_redo()),
|
||||
)
|
||||
.with_child(radio_group);
|
||||
|
||||
Flex::column()
|
||||
.cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
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()