use egui::Pos2;
use crate::screenshot::action::{
ActionContext, ActionResult, DrawingContext, RenderContext, ScreenAction, ToolCategory,
};
use crate::screenshot::stroke::{Stroke, StrokeStyle};
pub struct AnnotateAction {
active: bool,
is_drawing: bool,
}
impl AnnotateAction {
pub fn new() -> Self {
Self { active: false, is_drawing: false }
}
}
impl Default for AnnotateAction {
fn default() -> Self {
Self::new()
}
}
impl ScreenAction for AnnotateAction {
fn id(&self) -> &str {
"annotate"
}
fn name(&self) -> &str {
"Pencil"
}
fn icon_id(&self) -> Option<&str> {
Some("annotate")
}
fn category(&self) -> ToolCategory {
ToolCategory::Drawing
}
fn is_active(&self) -> bool {
self.active
}
fn set_active(&mut self, active: bool) {
self.active = active;
if !active {
self.is_drawing = false;
}
}
fn on_click(&mut self, _ctx: &ActionContext) -> ActionResult {
self.active = !self.active;
ActionResult::Continue
}
fn is_drawing_tool(&self) -> bool {
true
}
fn on_draw_start(&mut self, pos: Pos2, ctx: &mut DrawingContext) {
let clamped_pos = ctx.clamp_to_bounds(pos);
ctx.annotations.start_stroke(clamped_pos, ctx.settings);
self.is_drawing = true;
}
fn on_draw_move(&mut self, pos: Pos2, ctx: &mut DrawingContext) {
if !self.is_drawing {
return;
}
let clamped_pos = ctx.clamp_to_bounds(pos);
ctx.annotations.add_point(clamped_pos);
}
fn on_draw_end(&mut self, ctx: &mut DrawingContext) {
if !self.is_drawing {
return;
}
ctx.annotations.finish_stroke();
self.is_drawing = false;
}
fn render_annotations(&self, ctx: &RenderContext) {
for stroke in &ctx.annotations.strokes {
Self::render_stroke(ctx.ui, stroke);
}
if let Some(ref stroke) = ctx.annotations.current_stroke {
Self::render_stroke(ctx.ui, stroke);
}
}
}
impl AnnotateAction {
fn render_stroke(ui: &egui::Ui, stroke: &Stroke) {
if stroke.points.len() < 2 {
return;
}
let egui_stroke = egui::Stroke::new(stroke.settings.width, stroke.settings.color);
match stroke.settings.style {
StrokeStyle::Solid => {
for window in stroke.points.windows(2) {
ui.painter().line_segment([window[0], window[1]], egui_stroke);
}
}
StrokeStyle::Dashed => {
let dash_length = stroke.settings.width * 3.0;
let gap_length = stroke.settings.width * 2.0;
Self::render_dashed_line(ui, &stroke.points, egui_stroke, dash_length, gap_length);
}
StrokeStyle::Dotted => {
let dot_spacing = stroke.settings.width * 2.5;
Self::render_dotted_line(
ui,
&stroke.points,
stroke.settings.color,
stroke.settings.width / 2.0,
dot_spacing,
);
}
StrokeStyle::DashDot => {
let dash_length = stroke.settings.width * 3.0;
let gap_length = stroke.settings.width * 1.5;
let dot_radius = stroke.settings.width / 2.0;
Self::render_dash_dot_line(
ui,
&stroke.points,
egui_stroke,
stroke.settings.color,
dash_length,
gap_length,
dot_radius,
1,
);
}
StrokeStyle::DashDotDot => {
let dash_length = stroke.settings.width * 3.0;
let gap_length = stroke.settings.width * 1.5;
let dot_radius = stroke.settings.width / 2.0;
Self::render_dash_dot_line(
ui,
&stroke.points,
egui_stroke,
stroke.settings.color,
dash_length,
gap_length,
dot_radius,
2,
);
}
}
}
fn render_dashed_line(
ui: &egui::Ui,
points: &[Pos2],
stroke: egui::Stroke,
dash_length: f32,
gap_length: f32,
) {
if points.len() < 2 {
return;
}
let mut accumulated = 0.0;
let mut drawing = true;
let mut current_start = points[0];
for window in points.windows(2) {
let start = window[0];
let end = window[1];
let segment_vec = end - start;
let segment_length = segment_vec.length();
if segment_length < 0.001 {
continue;
}
let direction = segment_vec / segment_length;
let mut pos_along = 0.0;
while pos_along < segment_length {
let remaining_in_state =
if drawing { dash_length } else { gap_length } - accumulated;
let remaining_in_segment = segment_length - pos_along;
let step = remaining_in_state.min(remaining_in_segment);
if drawing {
let line_end = start + direction * (pos_along + step);
ui.painter().line_segment([current_start, line_end], stroke);
current_start = line_end;
} else {
current_start = start + direction * (pos_along + step);
}
pos_along += step;
accumulated += step;
if accumulated >= (if drawing { dash_length } else { gap_length }) {
drawing = !drawing;
accumulated = 0.0;
}
}
}
}
fn render_dotted_line(
ui: &egui::Ui,
points: &[Pos2],
color: egui::Color32,
radius: f32,
spacing: f32,
) {
if points.is_empty() {
return;
}
let mut accumulated = 0.0;
ui.painter().circle_filled(points[0], radius, color);
for window in points.windows(2) {
let start = window[0];
let end = window[1];
let segment_vec = end - start;
let segment_length = segment_vec.length();
if segment_length < 0.001 {
continue;
}
let direction = segment_vec / segment_length;
let mut pos_along = 0.0;
while pos_along < segment_length {
let remaining_to_next_dot = spacing - accumulated;
if remaining_to_next_dot <= segment_length - pos_along {
pos_along += remaining_to_next_dot;
let dot_pos = start + direction * pos_along;
ui.painter().circle_filled(dot_pos, radius, color);
accumulated = 0.0;
} else {
accumulated += segment_length - pos_along;
break;
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_dash_dot_line(
ui: &egui::Ui,
points: &[Pos2],
stroke: egui::Stroke,
dot_color: egui::Color32,
dash_length: f32,
gap_length: f32,
dot_radius: f32,
dots_per_cycle: usize,
) {
if points.len() < 2 {
return;
}
let mut state = 0usize;
let mut accumulated = 0.0;
let mut current_start = points[0];
for window in points.windows(2) {
let start = window[0];
let end = window[1];
let segment_vec = end - start;
let segment_length = segment_vec.length();
if segment_length < 0.001 {
continue;
}
let direction = segment_vec / segment_length;
let mut pos_along = 0.0;
while pos_along < segment_length {
let target_length = if state == 0 { dash_length } else { gap_length };
let remaining_in_state = target_length - accumulated;
let remaining_in_segment = segment_length - pos_along;
let step = remaining_in_state.min(remaining_in_segment);
if state == 0 {
let line_end = start + direction * (pos_along + step);
ui.painter().line_segment([current_start, line_end], stroke);
current_start = line_end;
} else {
current_start = start + direction * (pos_along + step);
}
pos_along += step;
accumulated += step;
if accumulated >= target_length {
if state > 0 && state <= dots_per_cycle {
ui.painter().circle_filled(current_start, dot_radius, dot_color);
}
state = (state + 1) % (1 + dots_per_cycle);
accumulated = 0.0;
}
}
}
}
}