Skip to main content

blockworx_editor/render/
hit_test.rs

1//! Bounding-box estimators for hit-testing pin/port labels and stubs. These
2//! mirror the geometry the drawing primitives use, but return the rect rather
3//! than painting — driving click/drag/marquee targets in `widget`. They measure
4//! text through whichever [`Renderer`] backend the caller holds, so a hit test
5//! is exercisable without an egui context.
6
7use crate::{
8    grid::{GRID_SIZE, pin_offset_y},
9    shape::pin::PinSide,
10    theme::Style,
11};
12use blockworx_geom::{Align, Align2, Pos2, Rect, pos2};
13use blockworx_paint::Renderer;
14
15use super::{
16    ADD_NAME_PLACEHOLDER, ADD_TYPE_PLACEHOLDER, first_line, pin_label_anchors, pin_tag_location,
17};
18
19/// Rect of a pin's **name** line at world anchor `pos`/`h_align`, positioned
20/// exactly as [`draw_pin_labels`](super::draw_pin_labels) (centered on the stub).
21/// An empty name is sized to the "Add Name" placeholder so the slot stays a
22/// click/placeholder target while the shape is selected.
23pub fn pin_name_rect_at(
24    pos: Pos2,
25    h_align: Align,
26    name: &str,
27    painter: &Style<'_, impl Renderer>,
28) -> Rect {
29    let name = first_line(name);
30    let text = if name.is_empty() {
31        ADD_NAME_PLACEHOLDER
32    } else {
33        name
34    };
35    let size = painter.text_size(text, &painter.theme().pin_font);
36    let (name_pos, _) = pin_label_anchors(pos);
37    Align2([h_align, Align::Center]).anchor_size(name_pos, size)
38}
39
40/// Rect of a pin's **type** line — the sibling of [`pin_name_rect_at`], measured
41/// in the smaller subtitle font and placed just below the name.
42pub fn pin_type_rect_at(
43    pos: Pos2,
44    h_align: Align,
45    type_label: &str,
46    painter: &Style<'_, impl Renderer>,
47) -> Rect {
48    let type_label = first_line(type_label);
49    let text = if type_label.is_empty() {
50        ADD_TYPE_PLACEHOLDER
51    } else {
52        type_label
53    };
54    let size = painter.text_size(text, &painter.theme().pin_subtitle_font);
55    let (_, type_pos) = pin_label_anchors(pos);
56    Align2([h_align, Align::Center]).anchor_size(type_pos, size)
57}
58
59/// World anchor (and horizontal alignment) of a block pin's labels, matching
60/// [`pin_text_location`](super::pin_text_location).
61fn block_pin_label_anchor(bbox: Rect, side: PinSide, offset: u32) -> (Pos2, Align) {
62    let y = pin_offset_y(bbox.top(), offset);
63    match side {
64        PinSide::East => (pos2(bbox.right() - GRID_SIZE / 2.0, y), Align::Max),
65        PinSide::West => (pos2(bbox.left() + GRID_SIZE / 2.0, y), Align::Min),
66    }
67}
68
69/// [`pin_name_rect_at`] for a block pin given its `side`/`offset`.
70pub fn estimate_bbox_for_pin_name(
71    bbox: Rect,
72    side: PinSide,
73    offset: u32,
74    name: &str,
75    painter: &Style<'_, impl Renderer>,
76) -> Rect {
77    let (pos, h) = block_pin_label_anchor(bbox, side, offset);
78    pin_name_rect_at(pos, h, name, painter)
79}
80
81/// [`pin_type_rect_at`] for a block pin given its `side`/`offset`.
82pub fn estimate_bbox_for_pin_type(
83    bbox: Rect,
84    side: PinSide,
85    offset: u32,
86    type_label: &str,
87    painter: &Style<'_, impl Renderer>,
88) -> Rect {
89    let (pos, h) = block_pin_label_anchor(bbox, side, offset);
90    pin_type_rect_at(pos, h, type_label, painter)
91}
92
93/// Bounding box of a whole pin as drawn — its stub plus the name and tag labels
94/// — at the given `side`/`offset`. Tools frame selected pins with this so the
95/// idle selection rectangle and the drag preview enclose the same parts and the
96/// frame doesn't change shape when a drag begins.
97pub struct PinExtent<'a> {
98    /// The owning shape's rect, which the pin hangs off.
99    pub bbox: Rect,
100    pub side: PinSide,
101    pub offset: u32,
102    pub name: &'a str,
103    pub type_label: &'a str,
104    pub tag: &'a str,
105}
106
107impl PinExtent<'_> {
108    pub fn bbox(&self, painter: &Style<'_, impl Renderer>) -> Rect {
109        let Self {
110            bbox,
111            side,
112            offset,
113            name,
114            type_label,
115            tag,
116        } = *self;
117        let line_y = pin_offset_y(bbox.top(), offset);
118        let mut rect = estimate_bbox_for_pin_stub(bbox.left(), bbox.right(), side, line_y)
119            .union(estimate_bbox_for_pin_name(
120                bbox, side, offset, name, painter,
121            ))
122            .union(estimate_bbox_for_pin_type(
123                bbox, side, offset, type_label, painter,
124            ));
125        if !tag.is_empty() {
126            rect = rect.union(
127                TagSlot {
128                    left: bbox.left(),
129                    right: bbox.right(),
130                    side,
131                    line_y,
132                }
133                .bbox(tag, painter),
134            );
135        }
136        rect
137    }
138}
139
140/// Hit region for a pin/port stub (the red line): the stub segment, widened to
141/// +/- `GRID_SIZE / 3` vertically so it presents an easy click target.
142/// `left`/`right` are the shape's edge x-coordinates; `line_y` the stub's y.
143pub fn estimate_bbox_for_pin_stub(left: f32, right: f32, side: PinSide, line_y: f32) -> Rect {
144    let half = GRID_SIZE / 3.0;
145    let (x0, x1) = match side {
146        PinSide::East => (right, right + GRID_SIZE),
147        PinSide::West => (left - GRID_SIZE, left),
148    };
149    Rect::from_min_max(pos2(x0, line_y - half), pos2(x1, line_y + half))
150}
151
152/// Shared tag-rect computation used by both block pins and ports. `left`/`right`
153/// are the shape's edge x-coordinates; `line_y` the stub's y.
154pub struct TagSlot {
155    /// The shape's edge x-coordinates (the inner end of the stub).
156    pub left: f32,
157    pub right: f32,
158    pub side: PinSide,
159    /// The stub's y.
160    pub line_y: f32,
161}
162
163impl TagSlot {
164    pub fn bbox(&self, tag: &str, painter: &Style<'_, impl Renderer>) -> Rect {
165        let size = painter.text_size(tag, &painter.theme().tag_font);
166        let (pos, align) = pin_tag_location(self.left, self.right, self.side, self.line_y);
167        align.anchor_size(pos, size)
168    }
169}