Rust · 4363 bytes Raw Blame History
1 //! Blur tool - pixelate regions for redaction.
2
3 use super::Tool;
4 use crate::annotate::state::ToolProperties;
5 use gartk_core::{InputEvent, MouseButton, Point, Rect};
6 use gartk_render::cairo::Context;
7 use gartk_render::stroke_rect;
8 use gartk_x11::CursorShape;
9
10 /// Blur/pixelate tool for redacting sensitive information.
11 pub struct BlurTool {
12 /// Starting corner of the region.
13 start: Option<Point>,
14 /// Opposite corner of the region.
15 end: Option<Point>,
16 /// Whether we're currently drawing.
17 drawing: bool,
18 }
19
20 impl BlurTool {
21 /// Create a new blur tool.
22 pub fn new() -> Self {
23 Self {
24 start: None,
25 end: None,
26 drawing: false,
27 }
28 }
29
30 /// Calculate normalized rectangle from start and end points.
31 pub fn calculate_rect(&self) -> Option<Rect> {
32 let (start, end) = (self.start?, self.end?);
33
34 let x = start.x.min(end.x);
35 let y = start.y.min(end.y);
36 let width = (end.x - start.x).unsigned_abs();
37 let height = (end.y - start.y).unsigned_abs();
38
39 if width > 0 && height > 0 {
40 Some(Rect::new(x, y, width, height))
41 } else {
42 None
43 }
44 }
45
46 /// Draw the selection rectangle outline.
47 fn draw_selection(&self, ctx: &Context, props: &ToolProperties) {
48 if let Some(rect) = self.calculate_rect() {
49 // Draw dashed outline to indicate blur region
50 ctx.set_dash(&[5.0, 5.0], 0.0);
51 stroke_rect(ctx, rect, props.color, 2.0);
52 ctx.set_dash(&[], 0.0);
53 }
54 }
55 }
56
57 impl Default for BlurTool {
58 fn default() -> Self {
59 Self::new()
60 }
61 }
62
63 impl Tool for BlurTool {
64 fn handle_event(&mut self, event: &InputEvent, _props: &ToolProperties) -> bool {
65 match event {
66 InputEvent::MousePress(e) if e.button == Some(MouseButton::Left) => {
67 self.start = Some(e.position);
68 self.end = Some(e.position);
69 self.drawing = true;
70 true
71 }
72 InputEvent::MouseMove(e) if self.drawing => {
73 self.end = Some(e.position);
74 true
75 }
76 InputEvent::MouseRelease(e) if e.button == Some(MouseButton::Left) && self.drawing => {
77 self.end = Some(e.position);
78 self.drawing = false;
79 true
80 }
81 _ => false,
82 }
83 }
84
85 fn draw_preview(&self, ctx: &Context, props: &ToolProperties) {
86 self.draw_selection(ctx, props);
87 }
88
89 fn commit(&self, ctx: &Context, props: &ToolProperties) {
90 // For the preview/commit, we draw a pixelated pattern
91 // The actual blur is applied by the overlay when committing
92 if let Some(rect) = self.calculate_rect() {
93 // Draw a checkerboard pattern to indicate blur
94 let block_size = props.blur_radius.max(8) as f64;
95
96 ctx.save().ok();
97 ctx.rectangle(
98 rect.x as f64,
99 rect.y as f64,
100 rect.width as f64,
101 rect.height as f64,
102 );
103 ctx.clip();
104
105 // Draw checkerboard pattern
106 let mut dark = true;
107 let mut y = rect.y as f64;
108 while y < (rect.y + rect.height as i32) as f64 {
109 let mut x = rect.x as f64;
110 let row_start_dark = dark;
111 while x < (rect.x + rect.width as i32) as f64 {
112 if dark {
113 ctx.set_source_rgba(0.2, 0.2, 0.2, 0.8);
114 } else {
115 ctx.set_source_rgba(0.3, 0.3, 0.3, 0.8);
116 }
117 ctx.rectangle(x, y, block_size, block_size);
118 let _ = ctx.fill();
119 dark = !dark;
120 x += block_size;
121 }
122 dark = !row_start_dark;
123 y += block_size;
124 }
125
126 ctx.restore().ok();
127 }
128 }
129
130 fn reset(&mut self) {
131 self.start = None;
132 self.end = None;
133 self.drawing = false;
134 }
135
136 fn cursor(&self) -> CursorShape {
137 CursorShape::Crosshair
138 }
139
140 fn is_drawing(&self) -> bool {
141 self.drawing
142 }
143
144 fn can_commit(&self) -> bool {
145 self.calculate_rect().is_some()
146 }
147 }
148