diff --git a/config.toml b/config.toml index 67515d9..b7f723f 100644 --- a/config.toml +++ b/config.toml @@ -15,6 +15,9 @@ output-filename = "/tmp/test-%Y-%m-%d_%H:%M:%S.png" save-after-copy = false # Hide toolbars by default default-hide-toolbars = false +# Whether to set block or line/pen as the default highlighter, other mode is accessible using CTRL. +default-block-highlight = true + # Font to use for text annotations [font] family = "Roboto" diff --git a/src/command_line.rs b/src/command_line.rs index 0aa9008..ef0c199 100644 --- a/src/command_line.rs +++ b/src/command_line.rs @@ -51,6 +51,10 @@ pub struct CommandLine { /// Font style to use for text annotations #[arg(long)] pub font_style: Option, + + /// Change the default highlighter to the line/pen highlighter. + #[arg(long)] + pub default_line_highlight: bool, } #[derive(Debug, Clone, Copy, Default, ValueEnum)] diff --git a/src/configuration.rs b/src/configuration.rs index acbd546..216f0f1 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -39,6 +39,7 @@ pub struct Configuration { color_palette: ColorPalette, default_hide_toolbars: bool, font: FontConfiguration, + default_block_highlight: bool, } #[derive(Default)] @@ -170,6 +171,9 @@ impl Configuration { if let Some(v) = general.default_hide_toolbars { self.default_hide_toolbars = v; } + if let Some(v) = general.default_block_highlight { + self.default_block_highlight = v; + } } fn merge(&mut self, file: Option, command_line: CommandLine) { // input_filename is required and needs to be overwritten @@ -219,6 +223,10 @@ impl Configuration { if let Some(v) = command_line.font_style { self.font.style = Some(v); } + + if command_line.default_line_highlight { + self.default_block_highlight = !command_line.default_line_highlight; + } } pub fn early_exit(&self) -> bool { @@ -261,6 +269,9 @@ impl Configuration { self.default_hide_toolbars } + pub fn default_block_highlight(&self) -> bool { + self.default_block_highlight + } pub fn font(&self) -> &FontConfiguration { &self.font } @@ -280,6 +291,7 @@ impl Default for Configuration { color_palette: ColorPalette::default(), default_hide_toolbars: false, font: FontConfiguration::default(), + default_block_highlight: true, } } } @@ -323,6 +335,7 @@ struct ConfiguationFileGeneral { output_filename: Option, save_after_copy: Option, default_hide_toolbars: Option, + default_block_highlight: Option, } #[derive(Deserialize)] diff --git a/src/main.rs b/src/main.rs index 9132343..32ad989 100644 --- a/src/main.rs +++ b/src/main.rs @@ -169,6 +169,17 @@ impl Component for App { glib::Propagation::Stop }, + connect_key_released[sketch_board_sender] => move |controller, key, code, modifier | { + if let Some(im_context) = controller.im_context() { + im_context.focus_in(); + if !im_context.filter_keypress(controller.current_event().unwrap()) { + sketch_board_sender.emit(SketchBoardInput::new_key_release_event(KeyEventMsg::new(key, code, modifier))); + } + } else { + sketch_board_sender.emit(SketchBoardInput::new_key_release_event(KeyEventMsg::new(key, code, modifier))); + } + }, + #[wrap(Some)] set_im_context = >k::IMMulticontext { connect_commit[sketch_board_sender] => move |_cx, txt| { diff --git a/src/sketch_board.rs b/src/sketch_board.rs index dbddc70..437d224 100644 --- a/src/sketch_board.rs +++ b/src/sketch_board.rs @@ -47,6 +47,7 @@ pub enum SketchBoardOutput { pub enum InputEvent { Mouse(MouseEventMsg), Key(KeyEventMsg), + KeyRelease(KeyEventMsg), Text(TextEventMsg), } @@ -105,6 +106,10 @@ impl SketchBoardInput { SketchBoardInput::InputEvent(InputEvent::Key(event)) } + pub fn new_key_release_event(event: KeyEventMsg) -> SketchBoardInput { + SketchBoardInput::InputEvent(InputEvent::KeyRelease(event)) + } + pub fn new_text_event(event: TextEventMsg) -> SketchBoardInput { SketchBoardInput::InputEvent(InputEvent::Text(event)) } diff --git a/src/style.rs b/src/style.rs index 1dcc045..51e9dd2 100644 --- a/src/style.rs +++ b/src/style.rs @@ -213,4 +213,8 @@ impl Size { Size::Large => 45.0 * size_factor, } } + + pub fn default_block_highlight(self) -> bool { + APP_CONFIG.read().default_block_highlight() + } } diff --git a/src/tools/highlight.rs b/src/tools/highlight.rs index 72eb86b..51eded3 100644 --- a/src/tools/highlight.rs +++ b/src/tools/highlight.rs @@ -1,72 +1,84 @@ +use std::ops::{Add, Sub}; + use anyhow::Result; use femtovg::{Paint, Path}; use relm4::gtk::gdk::{Key, ModifierType}; use crate::{ + configuration::APP_CONFIG, math::{self, Vec2D}, sketch_board::{MouseEventMsg, MouseEventType}, - style::{Size, Style}, + style::Style, + tools::DrawableClone, }; -use super::{Drawable, DrawableClone, Tool, ToolUpdateResult}; +use super::{Drawable, Tool, ToolUpdateResult}; + +const HIGHLIGHT_OPACITY: f64 = 0.4; #[derive(Clone, Debug)] -pub struct Highlight { +struct BlockHighlight { top_left: Vec2D, size: Option, - style: Style, - editing: bool, - points: Option>, +} + +#[derive(Clone, Debug)] +struct LineHighlight { + points: Vec, shift_pressed: bool, } -impl Highlight { - // This is triggered when a user does not press shift before highlighting. - fn draw_free_hand( - &self, - canvas: &mut femtovg::Canvas, - ) -> Result<()> { +#[derive(Clone, Debug)] +struct Highlighter { + data: T, + style: Style, +} + +trait Highlight { + fn highlight(&self, canvas: &mut femtovg::Canvas) -> Result<()>; +} + +impl Highlight for Highlighter { + fn highlight(&self, canvas: &mut femtovg::Canvas) -> Result<()> { canvas.save(); - let mut path = Path::new(); - if let Some(points) = &self.points { - let first = points.first().expect("atleast one point"); - path.move_to(first.x, first.y); - for p in points.iter().skip(1) { - path.line_to(first.x + p.x, first.y + p.y); - } - let mut paint = Paint::color(femtovg::Color::rgba( - self.style.color.r, - self.style.color.g, - self.style.color.b, - (255.0 * 0.4) as u8, - )); - paint.set_line_width(self.style.size.to_highlight_width()); + let mut path = Path::new(); + let first = self + .data + .points + .first() + .expect("should exist atleast one point in highlight instance."); - canvas.stroke_path(&path, &paint); + path.move_to(first.x, first.y); + for p in self.data.points.iter().skip(1) { + path.line_to(first.x + p.x, first.y + p.y); } + + let mut paint = Paint::color(femtovg::Color::rgba( + self.style.color.r, + self.style.color.g, + self.style.color.b, + (255.0 * HIGHLIGHT_OPACITY) as u8, + )); + paint.set_line_width(self.style.size.to_highlight_width()); + paint.set_line_join(femtovg::LineJoin::Round); + paint.set_line_cap(femtovg::LineCap::Square); + + canvas.stroke_path(&path, &paint); canvas.restore(); Ok(()) } +} - /// This is triggered when the user presses shift *before* highlighting. - fn draw_aligned(&self, canvas: &mut femtovg::Canvas) -> Result<()> { - let size = match self.size { +impl Highlight for Highlighter { + fn highlight(&self, canvas: &mut femtovg::Canvas) -> Result<()> { + let size = match self.data.size { Some(s) => s, None => return Ok(()), // early exit if size is none }; - let (pos, size) = math::rect_ensure_positive_size(self.top_left, size); - - if self.editing { - // include a border when selecting an area. - let border_paint = - Paint::color(self.style.color.into()).with_line_width(Size::Small.to_line_width()); - let mut border_path = Path::new(); - border_path.rect(pos.x, pos.y, size.x, size.y); - canvas.stroke_path(&border_path, &border_paint); - } + let (pos, size) = math::rect_ensure_positive_size(self.data.top_left, size); let mut shadow_path = Path::new(); shadow_path.rect(pos.x, pos.y, size.x, size.y); @@ -75,7 +87,7 @@ impl Highlight { self.style.color.r, self.style.color.g, self.style.color.b, - (255.0 * 0.4) as u8, + (255.0 * HIGHLIGHT_OPACITY) as u8, )); canvas.fill_path(&shadow_path, &shadow_paint); @@ -83,108 +95,167 @@ impl Highlight { } } -impl Drawable for Highlight { +#[derive(Clone, Debug)] +enum HighlightKind { + Block(Highlighter), + Line(Highlighter), +} +impl HighlightKind { + fn highlight(&self, canvas: &mut femtovg::Canvas) { + let _ = match self { + HighlightKind::Block(highlighter) => highlighter.highlight(canvas), + HighlightKind::Line(highlighter) => highlighter.highlight(canvas), + }; + } +} + +#[derive(Default, Clone, Debug)] +pub struct HighlightTool { + highlighter: Option, + style: Style, +} + +impl Drawable for HighlightKind { fn draw( &self, canvas: &mut femtovg::Canvas, _font: femtovg::FontId, ) -> Result<()> { - if self.points.is_some() { - self.draw_free_hand(canvas)?; - } else { - self.draw_aligned(canvas)?; - } + self.highlight(canvas); Ok(()) } } -#[derive(Default)] -pub struct HighlightTool { - highlight: Option, - style: Style, -} - impl Tool for HighlightTool { fn handle_mouse_event(&mut self, event: MouseEventMsg) -> ToolUpdateResult { let shift_pressed = event.modifier.intersects(ModifierType::SHIFT_MASK); let ctrl_pressed = event.modifier.intersects(ModifierType::CONTROL_MASK); + let default_highlight_block = APP_CONFIG.read().default_block_highlight(); match event.type_ { MouseEventType::BeginDrag => { - self.highlight = Some(Highlight { - top_left: event.pos, - size: None, - style: self.style, - editing: true, - points: if !ctrl_pressed { - Some(vec![event.pos]) - } else { - None - }, - shift_pressed, - }); + match (ctrl_pressed, default_highlight_block) { + (false, true) | (true, false) => { + self.highlighter = + Some(HighlightKind::Block(Highlighter:: { + data: BlockHighlight { + top_left: event.pos, + size: None, + }, + style: self.style, + })) + } + (false, false) | (true, true) => { + self.highlighter = Some(HighlightKind::Line(Highlighter:: { + data: LineHighlight { + points: vec![event.pos], + shift_pressed, + }, + style: self.style, + })) + } + } ToolUpdateResult::Redraw } - MouseEventType::EndDrag => { - if let Some(highlight) = &mut self.highlight { - if event.pos == Vec2D::zero() { - self.highlight = None; - - ToolUpdateResult::Redraw - } else { - if let Some(points) = &mut highlight.points { - if shift_pressed { - let last = points.last().expect("should have atleast one point"); - points.push(Vec2D::new(event.pos.x, last.y)); - } else { - points.push(event.pos); - } - } - - highlight.shift_pressed = shift_pressed; - highlight.editing = false; - - let result = highlight.clone_box(); - self.highlight = None; - - ToolUpdateResult::Commit(result) - } - } else { - ToolUpdateResult::Unmodified + MouseEventType::UpdateDrag | MouseEventType::EndDrag => { + if self.highlighter.is_none() { + return ToolUpdateResult::Unmodified; } - } - MouseEventType::UpdateDrag => { - if let Some(highlight) = &mut self.highlight { - if event.pos == Vec2D::zero() { - return ToolUpdateResult::Unmodified; + let mut highlighter_kind = self.highlighter.as_mut().unwrap(); + let update: ToolUpdateResult = match &mut highlighter_kind { + HighlightKind::Block(highlighter) => { + if shift_pressed { + let max_size = event.pos.x.abs().max(event.pos.y.abs()); + highlighter.data.size = Some(Vec2D { + x: max_size * event.pos.x.signum(), + y: max_size * event.pos.y.signum(), + }); + } else { + highlighter.data.size = Some(event.pos); + }; + ToolUpdateResult::Redraw } - if let Some(points) = &mut highlight.points { + HighlightKind::Line(highlighter) => { + if event.pos == Vec2D::zero() { + return ToolUpdateResult::Unmodified; + }; + if shift_pressed { - let last = points.last().expect("should have atleast one point"); - points.push(Vec2D::new(event.pos.x, last.y)); + // if shift was pressed before we remove an extra point which would + // have been the previous aligned point. However ignore if there is + // only one point which means the highlight has just started. + if highlighter.data.shift_pressed && highlighter.data.points.len() >= 2 + { + highlighter + .data + .points + .pop() + .expect("atleast 2 points in highlight path."); + }; + // use the last point to position the snapping guide, or 0 if the point + // is the first one. + let last = if highlighter.data.points.len() == 1 { + Vec2D::zero() + } else { + *highlighter + .data + .points + .last_mut() + .expect("atleast one point") + }; + let snapped_pos = event.pos.sub(last).snapped_vector_15deg().add(last); + highlighter.data.points.push(snapped_pos); } else { - points.push(event.pos); + highlighter.data.points.push(event.pos); } - } - highlight.size = Some(event.pos); - highlight.shift_pressed = shift_pressed; - ToolUpdateResult::Redraw - } else { - ToolUpdateResult::Unmodified - } + highlighter.data.shift_pressed = shift_pressed; + ToolUpdateResult::Redraw + } + }; + if event.type_ == MouseEventType::UpdateDrag { + return update; + }; + let result = highlighter_kind.clone_box(); + self.highlighter = None; + ToolUpdateResult::Commit(result) } + _ => ToolUpdateResult::Unmodified, } } fn handle_key_event(&mut self, event: crate::sketch_board::KeyEventMsg) -> ToolUpdateResult { - if event.key == Key::Escape && self.highlight.is_some() { - self.highlight = None; - ToolUpdateResult::Redraw - } else { - ToolUpdateResult::Unmodified + if event.key == Key::Escape && self.highlighter.is_some() { + self.highlighter = None; + return ToolUpdateResult::Redraw; } + ToolUpdateResult::Unmodified + } + + fn handle_key_release_event( + &mut self, + event: crate::sketch_board::KeyEventMsg, + ) -> ToolUpdateResult { + // add an extra point when shift is unheld, this allows for users to make sharper turns. + // press (aka: release) shift a second time to remove the added point. + if event.key == Key::Shift_L || event.key == Key::Shift_R { + if let Some(HighlightKind::Line(highlighter)) = &mut self.highlighter { + let points = &mut highlighter.data.points; + let last = points + .last() + .expect("line highlight must have atleast one point"); + if points.len() >= 2 { + if *last == points[points.len() - 2] { + points.pop(); + } else { + points.push(*last); + } + return ToolUpdateResult::Redraw; + }; + }; + } + ToolUpdateResult::Unmodified } fn handle_style_event(&mut self, style: Style) -> ToolUpdateResult { @@ -193,7 +264,7 @@ impl Tool for HighlightTool { } fn get_drawable(&self) -> Option<&dyn Drawable> { - match &self.highlight { + match &self.highlighter { Some(d) => Some(d), None => None, } diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 75ecfd6..8dfa8d9 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -56,6 +56,7 @@ pub trait Tool { match event { InputEvent::Mouse(e) => self.handle_mouse_event(e), InputEvent::Key(e) => self.handle_key_event(e), + InputEvent::KeyRelease(e) => self.handle_key_release_event(e), InputEvent::Text(e) => self.handle_text_event(e), } } @@ -75,6 +76,11 @@ pub trait Tool { ToolUpdateResult::Unmodified } + fn handle_key_release_event(&mut self, event: KeyEventMsg) -> ToolUpdateResult { + let _ = event; + ToolUpdateResult::Unmodified + } + fn handle_style_event(&mut self, style: Style) -> ToolUpdateResult { let _ = style; ToolUpdateResult::Unmodified