Skip to main content

utu/
marker.rs

1use std::collections::BTreeSet;
2
3use crate::{buffer::TextPosition, editor::Editor};
4
5#[derive(Debug, Clone)]
6pub enum Marker {
7    Stroke(StrokeMarker),
8    Line(LineMarker),
9    Rect(RectMarker),
10    FilledRect(FilledRectMarker),
11    Fill(FillMarker),
12}
13
14impl Marker {
15    pub fn new_stroke(editor: &Editor) -> Self {
16        Self::Stroke(StrokeMarker::new(editor))
17    }
18
19    pub fn new_line(editor: &Editor) -> Self {
20        Self::Line(LineMarker::new(editor))
21    }
22
23    pub fn new_rect(editor: &Editor) -> Self {
24        Self::Rect(RectMarker::new(editor))
25    }
26
27    pub fn new_fill(editor: &Editor) -> Self {
28        Self::Fill(FillMarker::new(editor))
29    }
30
31    pub fn new_filled_rect(editor: &Editor) -> Self {
32        Self::FilledRect(FilledRectMarker::new(editor))
33    }
34
35    pub fn name(&self) -> &'static str {
36        match self {
37            Marker::Stroke(_) => "MARK(STROKE)",
38            Marker::Line(_) => "MARK(LINE)",
39            Marker::Rect(_) => "MARK(RECT)",
40            Marker::FilledRect(_) => "MARK(FILLED_RECT)",
41            Marker::Fill(_) => "MARK(FILL)",
42        }
43    }
44
45    pub fn marked_positions(&self) -> Box<dyn '_ + Iterator<Item = TextPosition>> {
46        match self {
47            Marker::Stroke(m) => Box::new(m.positions.iter().copied()),
48            Marker::Line(m) => Box::new(m.marked_positions()),
49            Marker::Rect(m) => Box::new(m.marked_positions()),
50            Marker::FilledRect(m) => Box::new(m.marked_positions()),
51            Marker::Fill(m) => Box::new(m.filled_positions.iter().copied()),
52        }
53    }
54
55    pub fn handle_cursor_move(&mut self, editor: &Editor) {
56        match self {
57            Marker::Stroke(m) => m.handle_cursor_move(editor),
58            Marker::Line(m) => m.handle_cursor_move(editor),
59            Marker::Rect(m) => m.handle_cursor_move(editor),
60            Marker::FilledRect(m) => m.handle_cursor_move(editor),
61            Marker::Fill(m) => m.handle_cursor_move(editor),
62        }
63    }
64}
65
66#[derive(Debug, Clone)]
67pub struct StrokeMarker {
68    positions: BTreeSet<TextPosition>,
69}
70
71impl StrokeMarker {
72    fn new(editor: &Editor) -> Self {
73        Self {
74            positions: [editor.cursor].into_iter().collect(),
75        }
76    }
77
78    fn handle_cursor_move(&mut self, editor: &Editor) {
79        self.positions.insert(editor.cursor);
80    }
81}
82
83#[derive(Debug, Clone)]
84pub struct LineMarker {
85    start: TextPosition,
86    end: TextPosition,
87}
88
89impl LineMarker {
90    fn new(editor: &Editor) -> Self {
91        Self {
92            start: editor.cursor,
93            end: editor.cursor,
94        }
95    }
96
97    fn handle_cursor_move(&mut self, editor: &Editor) {
98        self.end = editor.cursor;
99    }
100
101    fn marked_positions(&self) -> impl Iterator<Item = TextPosition> + '_ {
102        let start = self.start;
103        let end = self.end;
104
105        // Calculate the line using simple interpolation
106        let dx = end.col as i32 - start.col as i32;
107        let dy = end.row as i32 - start.row as i32;
108
109        let steps = std::cmp::max(dx.abs(), dy.abs()) as usize;
110
111        (0..=steps).map(move |i| {
112            if steps == 0 {
113                return start;
114            }
115
116            let t = i as f64 / steps as f64;
117            let col = start.col as f64 + t * dx as f64;
118            let row = start.row as f64 + t * dy as f64;
119
120            TextPosition {
121                row: row.round() as usize,
122                col: col.round() as usize,
123            }
124        })
125    }
126}
127
128#[derive(Debug, Clone)]
129pub struct RectMarker {
130    start: TextPosition,
131    end: TextPosition,
132}
133
134impl RectMarker {
135    fn new(editor: &Editor) -> Self {
136        Self {
137            start: editor.cursor,
138            end: editor.cursor,
139        }
140    }
141
142    fn handle_cursor_move(&mut self, editor: &Editor) {
143        self.end = editor.cursor;
144    }
145
146    fn marked_positions(&self) -> impl Iterator<Item = TextPosition> + '_ {
147        let start = self.start;
148        let end = self.end;
149
150        // Calculate rectangle bounds
151        let min_row = std::cmp::min(start.row, end.row);
152        let max_row = std::cmp::max(start.row, end.row);
153        let min_col = std::cmp::min(start.col, end.col);
154        let max_col = std::cmp::max(start.col, end.col);
155
156        // Generate rectangle outline positions
157        (min_row..=max_row).flat_map(move |row| {
158            (min_col..=max_col).filter_map(move |col| {
159                // Only include positions on the rectangle outline
160                if row == min_row || row == max_row || col == min_col || col == max_col {
161                    Some(TextPosition { row, col })
162                } else {
163                    None
164                }
165            })
166        })
167    }
168}
169
170#[derive(Debug, Clone)]
171pub struct FilledRectMarker {
172    start: TextPosition,
173    end: TextPosition,
174}
175
176impl FilledRectMarker {
177    fn new(editor: &Editor) -> Self {
178        Self {
179            start: editor.cursor,
180            end: editor.cursor,
181        }
182    }
183
184    fn handle_cursor_move(&mut self, editor: &Editor) {
185        self.end = editor.cursor;
186    }
187
188    fn marked_positions(&self) -> impl Iterator<Item = TextPosition> + '_ {
189        let start = self.start;
190        let end = self.end;
191
192        // Calculate rectangle bounds
193        let min_row = std::cmp::min(start.row, end.row);
194        let max_row = std::cmp::max(start.row, end.row);
195        let min_col = std::cmp::min(start.col, end.col);
196        let max_col = std::cmp::max(start.col, end.col);
197
198        // Generate all positions within the rectangle (filled)
199        (min_row..=max_row)
200            .flat_map(move |row| (min_col..=max_col).map(move |col| TextPosition { row, col }))
201    }
202}
203
204#[derive(Debug, Clone)]
205pub struct FillMarker {
206    position: TextPosition,
207    target_char: Option<char>,
208    filled_positions: BTreeSet<TextPosition>,
209}
210
211impl FillMarker {
212    fn new(editor: &Editor) -> Self {
213        let mut marker = Self {
214            position: editor.cursor,
215            target_char: None, // Initialize as None, will be set on first update
216            filled_positions: BTreeSet::new(),
217        };
218        marker.update_filled_positions(editor);
219        marker
220    }
221
222    fn handle_cursor_move(&mut self, editor: &Editor) {
223        self.position = editor.cursor;
224        self.update_filled_positions(editor);
225    }
226
227    fn update_filled_positions(&mut self, editor: &Editor) {
228        self.filled_positions.clear();
229
230        let start_pos = self.position;
231
232        // Get the character at the current position (None for background/empty positions)
233        let target_char = editor.buffer.get_char_at(start_pos);
234
235        // Only update if the character has changed
236        if self.target_char != target_char {
237            self.target_char = target_char;
238        }
239
240        if self.target_char.is_none() {
241            self.filled_positions.clear();
242            return;
243        }
244
245        // Perform flood fill
246        self.flood_fill(editor, start_pos, self.target_char);
247    }
248
249    fn flood_fill(&mut self, editor: &Editor, start_pos: TextPosition, target_char: Option<char>) {
250        let mut stack = vec![start_pos];
251
252        while let Some(current_pos) = stack.pop() {
253            // Get character at current position (None for background/empty positions)
254            let current_char = editor.buffer.get_char_at(current_pos);
255
256            // If character doesn't match target or position already visited, skip
257            if current_char != target_char || self.filled_positions.contains(&current_pos) {
258                continue;
259            }
260
261            // Mark this position
262            self.filled_positions.insert(current_pos);
263
264            // Add adjacent positions to stack (4-directional)
265            // Up
266            if current_pos.row > 0 {
267                stack.push(TextPosition {
268                    row: current_pos.row - 1,
269                    col: current_pos.col,
270                });
271            }
272            // Down
273            stack.push(TextPosition {
274                row: current_pos.row + 1,
275                col: current_pos.col,
276            });
277            // Left
278            if current_pos.col > 0 {
279                stack.push(TextPosition {
280                    row: current_pos.row,
281                    col: current_pos.col - 1, // TODO: consider unicode width
282                });
283            }
284            // Right
285            stack.push(TextPosition {
286                row: current_pos.row,
287                col: current_pos.col + 1, // TODO: consider unicode width
288            });
289        }
290    }
291}