fltk_term/
canvas.rs

1use crate::cells::CellBuffer;
2use crate::styles::*;
3use fltk::app;
4use fltk::{
5    draw,
6    enums::{Align, Color, Font, FrameType},
7    group,
8    prelude::*,
9};
10use std::sync::{Arc, Mutex};
11
12#[derive(Clone, Copy, Debug, Default)]
13pub struct Selection {
14    pub start: Option<(usize, usize)>,
15    pub end: Option<(usize, usize)>,
16}
17
18pub struct TermCanvas {
19    f: group::Group,
20    buffer: Arc<Mutex<CellBuffer>>,
21    scroll: Option<group::Scroll>,
22    blink: Arc<Mutex<bool>>,
23    selection: Arc<Mutex<Selection>>,
24}
25
26impl TermCanvas {
27    pub fn new<L: Into<Option<&'static str>>>(x: i32, y: i32, w: i32, h: i32, label: L) -> Self {
28        let mut f = group::Group::new(x, y, w, h, label);
29        f.set_frame(FrameType::FlatBox);
30        let default_bg = BLACK;
31        let default_fg = WHITE;
32        Self {
33            f,
34            buffer: Arc::new(Mutex::new(CellBuffer::new(2000, default_bg, default_fg))),
35            scroll: None,
36            blink: Arc::new(Mutex::new(true)),
37            selection: Arc::new(Mutex::new(Selection::default())),
38        }
39    }
40
41    pub fn set_size(&mut self, w: i32, h: i32) {
42        self.f.set_size(w, h);
43    }
44
45    pub fn widget(&self) -> &group::Group {
46        &self.f
47    }
48
49    pub fn set_buffer(&mut self, buffer: Arc<Mutex<CellBuffer>>) {
50        self.buffer = buffer.clone();
51        let blink_state = self.blink.clone();
52        let selection = self.selection.clone();
53        // Rebind draw closure to capture buffer and optional scroll.
54        self.f.draw(move |f| {
55            let x = f.x();
56            let y = f.y();
57            let w = f.w();
58            let h = f.h();
59
60            draw::set_draw_color(BLACK);
61            draw::draw_rectf(x, y, w, h);
62            let mut font = Font::Courier;
63            let font_size = 14;
64            draw::set_font(font, font_size);
65            let line_h = draw::height().max(14);
66            let pad_x = 6;
67            let mut yy = y + line_h;
68            let char_w = ((draw::width("M") as f32).ceil() as i32).max(1);
69            let cols = ((w - 2 * pad_x).max(1)) / char_w.max(1);
70
71            // Snapshot selection range (visual coords)
72            let sel = {
73                let se = selection.lock().ok();
74                let (s, e) = if let Some(ref g) = se {
75                    (g.start, g.end)
76                } else {
77                    (None, None)
78                };
79                match (s, e) {
80                    (Some(mut a), Some(mut b)) => {
81                        if b < a {
82                            std::mem::swap(&mut a, &mut b);
83                        }
84                        Some((a, b))
85                    }
86                    _ => None,
87                }
88            };
89
90            if let Ok(buf) = buffer.lock() {
91                draw::set_draw_color(buf.default_bg);
92                draw::draw_rectf(x, y, w, h);
93                let mut vline_idx: usize = 0;
94                for line in buf.snapshot().iter() {
95                    // wrap-aware iteration
96                    let total_cols = line.len() as i32;
97                    let mut col_used = 0i32;
98                    let mut run: String = String::new();
99                    let mut cur_fg = WHITE;
100                    let mut cur_bg = BLACK;
101                    let mut cur_bold = false;
102                    let mut cur_underline = false;
103                    let mut first = true;
104                    let mut xx = x + pad_x;
105                    let mut run_start_col: i32 = 0; // visual column where current run starts
106                                                    // draw selection outline for this visual line
107                    if let Some(((ls, cs), (le, ce))) = sel {
108                        if vline_idx >= ls && vline_idx <= le {
109                            let start_col = if vline_idx == ls { cs as i32 } else { 0 };
110                            let end_col = if vline_idx == le { ce as i32 } else { cols - 1 };
111                            if end_col >= start_col {
112                                let sx = x + pad_x + start_col * char_w;
113                                let sw = (end_col - start_col + 1) * char_w;
114                                draw::set_draw_color(BLUE);
115                                draw::draw_rect(sx, yy - line_h, sw, line_h);
116                            }
117                        }
118                    }
119                    let mut draw_run = |run: &str,
120                                        fg: Color,
121                                        bg: Color,
122                                        bold: bool,
123                                        underline: bool,
124                                        xx: &mut i32,
125                                        yy: i32,
126                                        w: i32,
127                                        col_pos: i32,
128                                        vline: usize| {
129                        if run.is_empty() {
130                            return;
131                        }
132                        if *xx == x + pad_x {
133                            // clear whole visual line background at start of segment
134                            draw::set_draw_color(buf.default_bg);
135                            draw::draw_rectf(x + pad_x, yy - line_h, w - 2 * pad_x, line_h);
136                        }
137                        let (tw, _th) = draw::measure(run, false);
138                        // background for this run
139                        draw::set_draw_color(bg);
140                        draw::draw_rectf(*xx, yy - line_h, tw, line_h);
141                        // selection overlay (behind text)
142                        if let Some(((ls, cs), (le, ce))) = sel {
143                            if vline >= ls && vline <= le {
144                                let sel_start = if vline == ls { cs as i32 } else { 0 };
145                                let sel_end = if vline == le { ce as i32 } else { cols - 1 };
146                                let run_cols = run.chars().count() as i32;
147                                let overlap_start = sel_start.max(col_pos);
148                                let overlap_end = sel_end.min(col_pos + run_cols - 1);
149                                if overlap_end >= overlap_start {
150                                    let sx = x + pad_x + overlap_start * char_w;
151                                    let sw = (overlap_end - overlap_start + 1) * char_w;
152                                    draw::set_draw_color(BLUE);
153                                    draw::draw_rectf(sx, yy - line_h, sw, line_h);
154                                }
155                            }
156                        }
157                        // font weight
158                        font = if bold {
159                            Font::CourierBold
160                        } else {
161                            Font::Courier
162                        };
163                        draw::set_draw_color(fg);
164                        draw::draw_text2(run, *xx, yy - line_h, w - *xx, line_h, Align::Left);
165                        if underline {
166                            let underline_y = yy - 2;
167                            draw::set_draw_color(fg);
168                            draw::draw_line(*xx, underline_y, *xx + tw, underline_y);
169                        }
170                        *xx += tw;
171                    };
172
173                    for c in line.iter() {
174                        let (fg, bg) = (c.style.fg, c.style.bg);
175                        let bold = c.style.bold;
176                        let underline = c.style.underline;
177                        let (fg_eff, bg_eff) = if c.style.inverse { (bg, fg) } else { (fg, bg) };
178                        if first
179                            || fg_eff != cur_fg
180                            || bg_eff != cur_bg
181                            || bold != cur_bold
182                            || underline != cur_underline
183                        {
184                            if !first {
185                                draw_run(
186                                    &run,
187                                    cur_fg,
188                                    cur_bg,
189                                    cur_bold,
190                                    cur_underline,
191                                    &mut xx,
192                                    yy,
193                                    w,
194                                    run_start_col,
195                                    vline_idx,
196                                );
197                                run.clear();
198                            }
199                            cur_fg = fg_eff;
200                            cur_bg = bg_eff;
201                            cur_bold = bold;
202                            cur_underline = underline;
203                            first = false;
204                            // New run starts at current visual column
205                            run_start_col = (col_used % cols) as i32;
206                        }
207                        run.push(c.ch);
208                        col_used += 1;
209                        // soft-wrap when reaching cols
210                        if cols > 0 && col_used % cols == 0 {
211                            // flush current run and move to next visual line
212                            draw_run(
213                                &run,
214                                cur_fg,
215                                cur_bg,
216                                cur_bold,
217                                cur_underline,
218                                &mut xx,
219                                yy,
220                                w,
221                                run_start_col,
222                                vline_idx,
223                            );
224                            run.clear();
225                            yy += line_h;
226                            vline_idx += 1;
227                            run_start_col = 0;
228                            xx = x + pad_x;
229                            if yy > y + h {
230                                break;
231                            }
232                        }
233                    }
234                    if !run.is_empty() && yy <= y + h {
235                        draw_run(
236                            &run,
237                            cur_fg,
238                            cur_bg,
239                            cur_bold,
240                            cur_underline,
241                            &mut xx,
242                            yy,
243                            w,
244                            run_start_col,
245                            vline_idx,
246                        );
247                    }
248                    if total_cols == 0 {
249                        // clear empty visual line
250                        draw::set_draw_color(buf.default_bg);
251                        draw::draw_rectf(x + pad_x, yy - line_h, w - 2 * pad_x, line_h);
252                        yy += line_h;
253                        vline_idx += 1;
254                    } else if total_cols % cols != 0 {
255                        yy += line_h;
256                        vline_idx += 1;
257                    }
258                    if yy > y + h {
259                        break;
260                    }
261                }
262                // Cursor rendering (blinking block)
263                if let Ok(b) = blink_state.lock() {
264                    if *b {
265                        let (crow, ccol) = buf.cursor();
266                        // Map to visual position
267                        let cols = ((w - 2 * pad_x).max(1))
268                            / ((draw::width("M") as f32).ceil() as i32).max(1);
269                        let before_lines = buf
270                            .snapshot()
271                            .iter()
272                            .take(crow)
273                            .map(|l| (l.len().max(1) as i32 + cols - 1) / cols)
274                            .sum::<i32>();
275                        let seg = (ccol as i32) / cols;
276                        let col = (ccol as i32) % cols;
277                        let cx = x + pad_x + col * ((draw::width("M") as f32).ceil() as i32);
278                        let cy = y + line_h + (before_lines + seg) * line_h;
279                        if cy - line_h >= y && cy <= y + h {
280                            draw::set_draw_color(buf.default_fg);
281                            draw::draw_rectf(
282                                cx,
283                                cy - line_h,
284                                (draw::width("M") as f32).ceil() as i32,
285                                line_h,
286                            );
287                        }
288                    }
289                }
290                return;
291            }
292
293            // Fallback
294            draw::set_draw_color(WHITE);
295        });
296    }
297
298    pub fn set_scroll(&mut self, scroll: group::Scroll) {
299        self.scroll = Some(scroll.clone());
300        // If buffer already attached, rebind draw closure to capture scroll
301        let buf = self.buffer.clone();
302        self.set_buffer(buf);
303    }
304
305    pub fn start_blink(&mut self, interval: f64) {
306        let blink_state = self.blink.clone();
307        let mut frame = self.f.clone();
308        app::add_timeout3(interval, move |h| {
309            if let Ok(mut b) = blink_state.lock() {
310                *b = !*b;
311            }
312            frame.redraw();
313            app::repeat_timeout3(interval, h);
314        });
315    }
316
317    fn mouse_to_vpos(&self, x: i32, y: i32) -> (usize, usize) {
318        let line_h = draw::height().max(14);
319        let char_w = ((draw::width("M") as f32).ceil() as i32).max(1);
320        let pad_x = 6;
321        let col = ((x - self.f.x() - pad_x).max(0) / char_w) as usize;
322        let row = ((y - self.f.y()).max(0) / line_h) as usize;
323        (row, col)
324    }
325
326    pub fn begin_selection_at(&mut self, x: i32, y: i32) {
327        let vpos = self.mouse_to_vpos(x, y);
328        if let Ok(mut sel) = self.selection.lock() {
329            sel.start = Some(vpos);
330            sel.end = Some(vpos);
331        }
332        self.f.redraw();
333    }
334
335    pub fn update_selection_at(&mut self, x: i32, y: i32) {
336        let vpos = self.mouse_to_vpos(x, y);
337        if let Ok(mut sel) = self.selection.lock() {
338            sel.end = Some(vpos);
339        }
340        self.f.redraw();
341    }
342
343    pub fn clear_selection(&mut self) {
344        if let Ok(mut sel) = self.selection.lock() {
345            sel.start = None;
346            sel.end = None;
347        }
348        self.f.redraw();
349    }
350
351    pub fn copy_selection_to_clipboard(&self) {
352        let buf_arc = self.buffer.clone();
353        if let Ok(buf) = buf_arc.lock() {
354            let snap = buf.snapshot();
355            let char_w = ((draw::width("M") as f32).ceil() as i32).max(1);
356            let pad_x = 6;
357            let cols = ((self.f.w() - 2 * pad_x).max(1) / char_w).max(1) as usize;
358            // Build visual lines
359            let mut visual: Vec<Vec<char>> = Vec::new();
360            for line in snap.iter() {
361                let row: Vec<char> = line.iter().map(|c| c.ch).collect();
362                if row.is_empty() {
363                    visual.push(Vec::new());
364                    continue;
365                }
366                for chunk in row.chunks(cols) {
367                    visual.push(chunk.to_vec());
368                }
369            }
370            let (s, e) = if let Ok(sel) = self.selection.lock() {
371                (sel.start, sel.end)
372            } else {
373                (None, None)
374            };
375            if let (Some(mut a), Some(mut b)) = (s, e) {
376                if b < a {
377                    std::mem::swap(&mut a, &mut b);
378                }
379                let mut out = String::new();
380                for v in a.0..=b.0 {
381                    if v >= visual.len() {
382                        break;
383                    }
384                    let line = &visual[v];
385                    let start = if v == a.0 { a.1.min(line.len()) } else { 0 };
386                    let end = if v == b.0 {
387                        (b.1 + 1).min(line.len())
388                    } else {
389                        line.len()
390                    };
391                    if start < end {
392                        for ch in &line[start..end] {
393                            out.push(*ch);
394                        }
395                    }
396                    if v != b.0 {
397                        out.push('\n');
398                    }
399                }
400                if !out.is_empty() {
401                    app::copy(&out);
402                }
403            }
404        };
405    }
406
407    #[allow(clippy::type_complexity)]
408    pub fn selection_handle(&self) -> Arc<Mutex<Selection>> {
409        self.selection.clone()
410    }
411
412    pub fn buffer_arc(&self) -> Arc<Mutex<CellBuffer>> {
413        self.buffer.clone()
414    }
415}
416
417fltk::widget_extends!(TermCanvas, group::Group, f);