Skip to main content

mimick/library/lightbox/ocr/
layer.rs

1//! Drawing area that outlines OCR boxes over the lightbox photo.
2
3use std::cell::RefCell;
4use std::rc::Rc;
5
6use gtk::prelude::*;
7
8use super::geometry::{contain_rect, quad_in_area};
9use super::state::OcrState;
10use crate::api_client::OcrBox;
11
12/// Non-interactive drawing area over the picture stack; it scrolls and zooms with the photo.
13pub(crate) struct OcrLayer {
14    pub(crate) area: gtk::DrawingArea,
15    state: Rc<RefCell<OcrState>>,
16}
17
18impl OcrLayer {
19    pub(crate) fn new() -> Self {
20        let area = gtk::DrawingArea::builder()
21            .hexpand(true)
22            .vexpand(true)
23            .can_target(false)
24            .visible(false)
25            .build();
26        let state = Rc::new(RefCell::new(OcrState::default()));
27        let draw_state = state.clone();
28        area.set_draw_func(move |_, cr, width, height| {
29            draw_boxes(
30                cr,
31                &draw_state.borrow(),
32                (f64::from(width), f64::from(height)),
33            );
34        });
35        Self { area, state }
36    }
37
38    /// Replaces the boxes and clears the selection; `image` is the displayed texture's size.
39    pub(super) fn set_boxes(&self, boxes: Vec<OcrBox>, image: (f64, f64)) {
40        *self.state.borrow_mut() = OcrState::new(boxes, image);
41        self.area.queue_draw();
42    }
43
44    pub(crate) fn clear(&self) {
45        *self.state.borrow_mut() = OcrState::default();
46        self.area.queue_draw();
47    }
48
49    pub(super) fn has_boxes(&self) -> bool {
50        !self.state.borrow().boxes().is_empty()
51    }
52
53    /// Box under `p`, given in this area's coordinates.
54    pub(super) fn box_at(&self, p: (f64, f64)) -> Option<usize> {
55        let area = (f64::from(self.area.width()), f64::from(self.area.height()));
56        self.state.borrow().box_at(area, p)
57    }
58
59    /// Returns whether the hovered box changed.
60    pub(super) fn set_hovered(&self, idx: Option<usize>) -> bool {
61        let changed = self.state.borrow_mut().set_hovered(idx);
62        if changed {
63            self.area.queue_draw();
64        }
65        changed
66    }
67
68    /// Mouse press on a box; returns whether dragging should extend the selection.
69    pub(super) fn press(&self, idx: usize, shift: bool, ctrl: bool) -> bool {
70        let extends = self.state.borrow_mut().press(idx, shift, ctrl);
71        self.area.queue_draw();
72        extends
73    }
74
75    pub(super) fn click(&self, idx: usize, extend: bool) {
76        self.state.borrow_mut().click(idx, extend);
77        self.area.queue_draw();
78    }
79
80    pub(super) fn extend_to(&self, idx: usize) {
81        self.state.borrow_mut().extend_to(idx);
82        self.area.queue_draw();
83    }
84
85    pub(super) fn has_selection(&self) -> bool {
86        self.state.borrow().has_selection()
87    }
88
89    pub(super) fn all_text(&self) -> Option<String> {
90        self.state.borrow().all_text()
91    }
92
93    pub(super) fn select_all(&self) {
94        self.state.borrow_mut().select_all();
95        self.area.queue_draw();
96    }
97
98    pub(super) fn clear_selection(&self) {
99        self.state.borrow_mut().clear_selection();
100        self.area.queue_draw();
101    }
102
103    pub(super) fn selected_text(&self) -> Option<String> {
104        self.state.borrow().selected_text()
105    }
106
107    pub(super) fn text_at(&self, idx: usize) -> Option<String> {
108        self.state
109            .borrow()
110            .boxes()
111            .get(idx)
112            .map(|ocr_box| ocr_box.text.clone())
113    }
114}
115
116/// Outlines every box in the accent colour, filled more strongly when hovered or selected.
117fn draw_boxes(cr: &gtk::cairo::Context, state: &OcrState, area: (f64, f64)) {
118    let Some(rect) = contain_rect(area, state.image()) else {
119        return;
120    };
121    let accent = libadwaita::StyleManager::default().accent_color().to_rgba();
122    let (r, g, b) = (
123        f64::from(accent.red()),
124        f64::from(accent.green()),
125        f64::from(accent.blue()),
126    );
127    for (idx, ocr_box) in state.boxes().iter().enumerate() {
128        let quad = quad_in_area(rect, ocr_box.corners());
129        cr.move_to(quad[0].0, quad[0].1);
130        for &(x, y) in &quad[1..] {
131            cr.line_to(x, y);
132        }
133        cr.close_path();
134        let active = state.is_selected(idx) || state.hovered() == Some(idx);
135        let (fill_alpha, line_width) = if active { (0.35, 2.5) } else { (0.12, 1.5) };
136        cr.set_source_rgba(r, g, b, fill_alpha);
137        let _ = cr.fill_preserve();
138        cr.set_source_rgba(r, g, b, 1.0);
139        cr.set_line_width(line_width);
140        let _ = cr.stroke();
141    }
142}