use std::ops::{Add, Sub};
use anyhow::Result;
use femtovg::{Paint, Path};
use relm4::{
Sender,
gtk::gdk::{Key, ModifierType},
};
use serde_derive::Deserialize;
use crate::{
configuration::APP_CONFIG,
math::{self, Vec2D},
sketch_board::{MouseButton, MouseEventMsg, MouseEventType, SketchBoardInput},
style::Style,
tools::DrawableClone,
};
use satty_cli::command_line;
use super::{
Drawable, Tool, ToolUpdateResult, Tools,
drag_box::{DragBox, draw_center_marker},
};
const HIGHLIGHT_OPACITY: f64 = 0.4;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Highlighters {
Block = 0,
Freehand = 1,
}
impl From<command_line::Highlighters> for Highlighters {
fn from(tool: command_line::Highlighters) -> Self {
match tool {
command_line::Highlighters::Block => Self::Block,
command_line::Highlighters::Freehand => Self::Freehand,
}
}
}
#[derive(Clone, Debug)]
struct BlockHighlight {
origin: Vec2D,
top_left: Vec2D,
size: Option<Vec2D>,
centered: bool,
finishing: bool,
}
#[derive(Clone, Debug)]
struct FreehandHighlight {
points: Vec<Vec2D>,
shift_pressed: bool,
}
#[derive(Clone, Debug)]
struct Highlighter<T> {
data: T,
style: Style,
}
trait Highlight {
fn highlight(&self, canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>) -> Result<()>;
}
impl Highlight for Highlighter<FreehandHighlight> {
fn highlight(&self, canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>) -> Result<()> {
canvas.save();
let mut path = Path::new();
let first = self
.data
.points
.first()
.expect("should exist at least one point in highlight instance.");
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(self.style.annotation_size_factor),
);
paint.set_line_join(femtovg::LineJoin::Round);
paint.set_line_cap(femtovg::LineCap::Square);
canvas.stroke_path(&path, &paint);
canvas.restore();
Ok(())
}
}
impl Highlight for Highlighter<BlockHighlight> {
fn highlight(&self, canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>) -> Result<()> {
let size = match self.data.size {
Some(s) => s,
None => return Ok(()), };
let (pos, size) = math::rect_ensure_positive_size(self.data.top_left, size);
if !self.data.finishing && self.data.centered {
draw_center_marker(canvas, self.data.origin);
}
let mut shadow_path = Path::new();
shadow_path.rounded_rect(
pos.x,
pos.y,
size.x,
size.y,
APP_CONFIG.read().corner_roundness(),
);
let shadow_paint = Paint::color(femtovg::Color::rgba(
self.style.color.r,
self.style.color.g,
self.style.color.b,
(255.0 * HIGHLIGHT_OPACITY) as u8,
));
canvas.fill_path(&shadow_path, &shadow_paint);
Ok(())
}
}
impl BlockHighlight {
fn calculate_shape(&mut self, pos: Vec2D, modifier: ModifierType) {
let drag_box = DragBox::from_origin_delta(self.origin, pos, modifier);
self.centered = drag_box.centered;
self.top_left = drag_box.top_left;
self.size = Some(drag_box.size);
}
}
#[derive(Clone, Debug)]
enum HighlightKind {
Block(Highlighter<BlockHighlight>),
Freehand(Highlighter<FreehandHighlight>),
}
#[derive(Default, Clone, Debug)]
pub struct HighlightTool {
highlighter: Option<HighlightKind>,
style: Style,
input_enabled: bool,
sender: Option<Sender<SketchBoardInput>>,
}
impl Drawable for HighlightKind {
fn draw(
&self,
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
_font: femtovg::FontId,
_bounds: (Vec2D, Vec2D),
) -> Result<()> {
match self {
HighlightKind::Block(highlighter) => highlighter.highlight(canvas),
HighlightKind::Freehand(highlighter) => highlighter.highlight(canvas),
}
}
}
impl Tool for HighlightTool {
fn input_enabled(&self) -> bool {
self.input_enabled
}
fn set_input_enabled(&mut self, value: bool) {
self.input_enabled = value;
}
fn active(&self) -> bool {
self.highlighter.is_some()
}
fn get_tool_type(&self) -> super::Tools {
Tools::Highlight
}
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 primary_highlighter = APP_CONFIG.read().primary_highlighter();
match event.type_ {
MouseEventType::BeginDrag => {
if event.button == MouseButton::Middle {
return ToolUpdateResult::Unmodified;
}
match (primary_highlighter, ctrl_pressed) {
(Highlighters::Block, false) | (Highlighters::Freehand, true) => {
self.highlighter =
Some(HighlightKind::Block(Highlighter::<BlockHighlight> {
data: BlockHighlight {
origin: event.pos,
top_left: event.pos,
size: None,
centered: false,
finishing: false,
},
style: self.style,
}))
}
(Highlighters::Freehand, false) | (Highlighters::Block, true) => {
self.highlighter =
Some(HighlightKind::Freehand(Highlighter::<FreehandHighlight> {
data: FreehandHighlight {
points: vec![event.pos],
shift_pressed,
},
style: self.style,
}))
}
}
ToolUpdateResult::Redraw
}
MouseEventType::UpdateDrag | MouseEventType::EndDrag => {
if event.button == MouseButton::Middle {
return ToolUpdateResult::Unmodified;
}
if self.highlighter.is_none() {
return ToolUpdateResult::Unmodified;
}
let mut highlighter_kind = self.highlighter.as_mut().unwrap();
let update: ToolUpdateResult = match &mut highlighter_kind {
HighlightKind::Block(highlighter) => {
highlighter.data.calculate_shape(event.pos, event.modifier);
ToolUpdateResult::Redraw
}
HighlightKind::Freehand(highlighter) => {
if event.pos == Vec2D::zero() {
return ToolUpdateResult::Unmodified;
};
if shift_pressed {
if highlighter.data.shift_pressed && highlighter.data.points.len() >= 2
{
highlighter
.data
.points
.pop()
.expect("at least 2 points in highlight path.");
};
let last = if highlighter.data.points.len() == 1 {
Vec2D::zero()
} else {
*highlighter
.data
.points
.last_mut()
.expect("at least one point")
};
let snapped_pos = event.pos.sub(last).snapped_vector_15deg().add(last);
highlighter.data.points.push(snapped_pos);
} else {
highlighter.data.points.push(event.pos);
}
highlighter.data.shift_pressed = shift_pressed;
ToolUpdateResult::Redraw
}
};
if event.type_ == MouseEventType::UpdateDrag {
return update;
}
if let HighlightKind::Block(highlighter) = &mut *highlighter_kind {
highlighter.data.finishing = true;
}
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.highlighter.is_some() {
self.highlighter = None;
return ToolUpdateResult::Redraw;
}
ToolUpdateResult::Unmodified
}
fn handle_key_release_event(
&mut self,
event: crate::sketch_board::KeyEventMsg,
) -> ToolUpdateResult {
if (event.key == Key::Shift_L || event.key == Key::Shift_R)
&& let Some(HighlightKind::Freehand(highlighter)) = &mut self.highlighter
{
let points = &mut highlighter.data.points;
let last = points
.last()
.expect("line highlight must have at least 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 {
self.style = style;
ToolUpdateResult::Unmodified
}
fn get_drawable(&self) -> Option<&dyn Drawable> {
match &self.highlighter {
Some(d) => Some(d),
None => None,
}
}
fn set_sender(&mut self, sender: Sender<SketchBoardInput>) {
self.sender = Some(sender);
}
}