Start dirty implementation of an eraser
This commit is contained in:
parent
9943681465
commit
be11d8e6ee
56
src/lib.rs
56
src/lib.rs
|
|
@ -36,16 +36,61 @@ impl CanvasElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use im::Vector;
|
// 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
|
// A canvas contains all elements to be drawn
|
||||||
pub type Canvas = Vector<CanvasElement>;
|
#[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)]
|
#[derive(Clone, druid::Data)]
|
||||||
pub struct VersionedCanvas {
|
pub struct VersionedCanvas {
|
||||||
// We internally guarantee that this vector
|
// We internally guarantee that this vector
|
||||||
// is never empty
|
// is never empty
|
||||||
versions: Vector<Canvas>,
|
versions: im::Vector<Canvas>,
|
||||||
curr_version: usize,
|
curr_version: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,7 +104,6 @@ impl VersionedCanvas {
|
||||||
|
|
||||||
// Get current canvas version
|
// Get current canvas version
|
||||||
pub fn get(&self) -> &Canvas {
|
pub fn get(&self) -> &Canvas {
|
||||||
let focus = self.versions.focus();
|
|
||||||
self.versions.get(self.curr_version).unwrap()
|
self.versions.get(self.curr_version).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,6 +115,10 @@ impl VersionedCanvas {
|
||||||
self.curr_version > 0
|
self.curr_version > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn version(&self) -> usize {
|
||||||
|
self.curr_version
|
||||||
|
}
|
||||||
|
|
||||||
pub fn undo(&mut self) {
|
pub fn undo(&mut self) {
|
||||||
if self.has_older_versions() {
|
if self.has_older_versions() {
|
||||||
self.curr_version = self.curr_version - 1;
|
self.curr_version = self.curr_version - 1;
|
||||||
|
|
|
||||||
143
src/main.rs
143
src/main.rs
|
|
@ -14,54 +14,67 @@
|
||||||
// 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, Rect};
|
||||||
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, LocalizedString, WindowDesc};
|
||||||
|
|
||||||
use stiletto::{CanvasElement, VersionedCanvas, Canvas};
|
use stiletto::{CanvasElement, VersionedCanvas, Canvas};
|
||||||
|
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Data)]
|
#[derive(Clone, Data)]
|
||||||
struct CanvasData {
|
struct CanvasData {
|
||||||
current_element: Option<CanvasElement>,
|
current_tool: CanvasTool,
|
||||||
//elements: Vector<CanvasElement>,
|
|
||||||
elements: VersionedCanvas,
|
elements: VersionedCanvas,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CanvasData {
|
impl CanvasData {
|
||||||
fn is_drawing(&self) -> bool {
|
/*fn is_drawing(&self) -> bool {
|
||||||
self.current_element.is_some()
|
self.current_element.is_some()
|
||||||
|
}*/
|
||||||
|
|
||||||
|
fn set_tool(&mut self, tool: CanvasTool) {
|
||||||
|
self.current_tool = tool;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perform_undo(&mut self) {
|
fn perform_undo(&mut self) {
|
||||||
if !self.is_drawing() {
|
|
||||||
self.elements.undo();
|
self.elements.undo();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn perform_redo(&mut self) {
|
fn perform_redo(&mut self) {
|
||||||
if !self.is_drawing() {
|
|
||||||
self.elements.redo();
|
self.elements.redo();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CanvasWidget;
|
fn handle_tool_event(&mut self, event: &Event) {
|
||||||
|
match &mut self.current_tool {
|
||||||
impl Widget<CanvasData> for CanvasWidget {
|
CanvasTool::Pen { current_element } => {
|
||||||
fn event(&mut self, _ctx: &mut EventCtx, event: &Event, data: &mut CanvasData, _env: &Env) {
|
|
||||||
match event {
|
match event {
|
||||||
Event::MouseDown(mouse_event) => {
|
Event::MouseDown(mouse_event) => {
|
||||||
let mut kurbo_path = BezPath::new();
|
let mut kurbo_path = BezPath::new();
|
||||||
kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y));
|
kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y));
|
||||||
data.current_element = Some(CanvasElement::Freehand {
|
*current_element = Some(CanvasElement::Freehand {
|
||||||
path: stiletto::Path { kurbo_path },
|
path: stiletto::Path { kurbo_path },
|
||||||
thickness: 2.0,
|
thickness: 2.0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Event::MouseMove(mouse_event) => {
|
Event::MouseMove(mouse_event) => {
|
||||||
if data.is_drawing() {
|
if current_element.is_some() {
|
||||||
if let Some(current_element) = data.current_element.as_mut() {
|
if let Some(current_element) = current_element.as_mut() {
|
||||||
current_element
|
current_element
|
||||||
.get_path_mut()
|
.get_path_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
@ -71,10 +84,10 @@ impl Widget<CanvasData> for CanvasWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::MouseUp(_) => {
|
Event::MouseUp(_) => {
|
||||||
if data.is_drawing() {
|
if current_element.is_some() {
|
||||||
if let Some(current_element) = data.current_element.take() {
|
if let Some(current_element) = current_element.take() {
|
||||||
|
|
||||||
data.elements.update(move |canvas: &Canvas| -> Canvas {
|
self.elements.update(move |canvas: &Canvas| -> Canvas {
|
||||||
let mut new_canvas = canvas.clone();
|
let mut new_canvas = canvas.clone();
|
||||||
new_canvas.push_back(current_element);
|
new_canvas.push_back(current_element);
|
||||||
return new_canvas;
|
return new_canvas;
|
||||||
|
|
@ -85,6 +98,47 @@ impl Widget<CanvasData> for CanvasWidget {
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
fn lifecycle(
|
||||||
|
|
@ -103,18 +157,36 @@ impl Widget<CanvasData> for CanvasWidget {
|
||||||
data: &CanvasData,
|
data: &CanvasData,
|
||||||
_env: &Env,
|
_env: &Env,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
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{ 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);
|
||||||
|
} else {
|
||||||
|
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 +215,31 @@ 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.elements.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, 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, data: &mut CanvasData, _env| data.perform_undo()))
|
||||||
Button::new("Undo").on_click(|_ctx: &mut EventCtx, data: &mut CanvasData, _env: &Env| data.perform_undo())
|
.with_child(Button::new("Redo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_redo()))
|
||||||
)
|
.with_child(Button::new("Pen").on_click(|_ctx, data: &mut CanvasData, _env| data.set_tool(CanvasTool::new_pen())))
|
||||||
.with_child(Button::new("Redo").on_click(|_ctx: &mut EventCtx, data: &mut CanvasData, _env: &Env| data.perform_redo()));
|
.with_child(Button::new("Eraser").on_click(|_ctx, data: &mut CanvasData, _env| data.set_tool(CanvasTool::new_eraser())));
|
||||||
|
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.cross_axis_alignment(CrossAxisAlignment::Center)
|
.cross_axis_alignment(CrossAxisAlignment::Center)
|
||||||
|
|
@ -176,8 +255,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![]),
|
elements: VersionedCanvas::new(Canvas::new()),
|
||||||
};
|
};
|
||||||
AppLauncher::with_window(window)
|
AppLauncher::with_window(window)
|
||||||
.use_simple_logger()
|
.use_simple_logger()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue