// 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 druid::widget::prelude::*; use druid::{ AppLauncher, Color, Data, Event, Lens, LensExt, LocalizedString, WidgetExt, WindowDesc, }; use stiletto::canvas::Canvas; use stiletto::canvas_tools::CanvasTool; use stiletto::versioned_canvas::VersionedCanvas; #[derive(Clone, Data, Lens)] struct CanvasData { #[lens(name = "current_tool_lens")] current_tool: CanvasTool, elements: VersionedCanvas, } impl CanvasData { fn perform_undo(&mut self) { self.elements.undo(); } fn perform_redo(&mut self) { self.elements.redo(); } fn handle_tool_event(&mut self, event: &Event) { self.current_tool.handle_event(event, &mut self.elements); } } struct CanvasWidget; impl Widget for CanvasWidget { fn event(&mut self, _ctx: &mut EventCtx, event: &Event, data: &mut CanvasData, _env: &Env) { data.handle_tool_event(event); } 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, ) { // 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 if old_element.is_some() && !new_element.is_some() { return; } if new_element.is_some() { if let Some(e) = new_element.as_ref() { ctx.request_paint_rect(e.bounding_box()); } } else { 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( &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().elements.iter() { element.draw(ctx); } match &data.current_tool { CanvasTool::Pen { current_element } => { if let Some(element) = ¤t_element { 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, } // 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 { 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); Flex::column() .cross_axis_alignment(CrossAxisAlignment::Center) .must_fill_main_axis(true) .with_child(SizedBox::new(Align::left(toolbar)).height(50.0)) .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_tool: CanvasTool::new_pen(), elements: VersionedCanvas::new(Canvas::new()), }; AppLauncher::with_window(window) .use_simple_logger() .launch(canvas_data) .expect("launch failed"); }