Skip to main content

blockworx/shape/
port.rs

1use crate::theme::Style;
2use blockworx_geom::{Align, Pos2, Rect, Vec2, pos2};
3use blockworx_paint::Renderer;
4
5use blockworx_doc::{block_model::Pin, id::PinId};
6
7use crate::edit::naming::Authoring;
8use crate::{
9    edit::lower::accent_from_role,
10    grid::{GRID_SIZE, px_rect, snap_rect},
11    render::draw_selection_frame,
12    shape::{
13        BaseShape,
14        block::resize_rect,
15        pin::{PinSide, orientation},
16    },
17    state::RenderMode,
18    theme::{Role, accent_role},
19};
20
21/// The canonical interior height of a port body, in grid cells.
22pub(crate) const PORT_HEIGHT: u32 = 2;
23
24/// Default/minimum port width in grid units. Gives the default `"Port"`
25/// name room to render.
26pub const MIN_PORT_WIDTH: u32 = 5;
27
28/// Minimum port width (grid units) that comfortably fits the pin's labels:
29/// half the longer of `name` / `type_label`, floored at [`MIN_PORT_WIDTH`].
30pub fn width_for_labels(name: &str, type_label: &str) -> u32 {
31    let longest = name.chars().count().max(type_label.chars().count());
32    (longest as u32 / 2).max(MIN_PORT_WIDTH)
33}
34
35/// One pin drawn as its own boundary *port*: the body its `rect` register
36/// places inside its owner's interior view, facing by
37/// [`crate::shape::pin::orientation`]. The same pin seen from
38/// the parent scope is a stub on its owner's rect instead — that drawing
39/// goes through [`BlockShape`](crate::shape::BlockShape), and
40/// [`Drawing::pin_shape`](crate::widget::drawing::Drawing::pin_shape)
41/// decides which of the two a `PinId` names here.
42#[derive(Clone, Copy)]
43pub struct PortShape<'a> {
44    pub id: PinId,
45    pub pin: &'a Pin,
46}
47
48impl<'a> PortShape<'a> {
49    fn body(&self) -> Rect {
50        px_rect(self.pin.rect)
51    }
52
53    /// The pin, iff `id` names this very port. Every pin accessor is
54    /// single-entry for a port: the shape *is* one pin.
55    fn matching(&self, id: PinId) -> Option<&'a Pin> {
56        (id == self.id).then_some(self.pin)
57    }
58}
59
60impl BaseShape for PortShape<'_> {
61    fn gui_rect(&self) -> Rect {
62        self.body()
63    }
64    fn constrain_resize_delta(&self, mut delta: Vec2) -> Vec2 {
65        delta.y = 0.0;
66        delta
67    }
68    fn pin(&self, id: PinId) -> Option<&Pin> {
69        self.matching(id)
70    }
71    fn anchor_point_with_rect(&self, rect: Rect, id: PinId) -> Option<Pos2> {
72        self.matching(id)?;
73        Some(match orientation(self.pin) {
74            PinSide::East => pos2(rect.right() + GRID_SIZE, rect.top() + GRID_SIZE),
75            PinSide::West => pos2(rect.left() - GRID_SIZE, rect.top() + GRID_SIZE),
76        })
77    }
78    fn pin_text_rect<R: Renderer>(&self, id: PinId, painter: &Style<'_, R>) -> Option<Rect> {
79        self.matching(id)?;
80        let center = crate::render::port_text_center(self.body(), orientation(self.pin));
81        Some(crate::render::pin_name_rect_at(
82            center,
83            Align::Center,
84            &self.pin.name,
85            painter,
86        ))
87    }
88    fn pin_type_rect<R: Renderer>(&self, id: PinId, painter: &Style<'_, R>) -> Option<Rect> {
89        self.matching(id)?;
90        let center = crate::render::port_text_center(self.body(), orientation(self.pin));
91        Some(crate::render::pin_type_rect_at(
92            center,
93            Align::Center,
94            &self.pin.type_name,
95            painter,
96        ))
97    }
98    fn tag_text_rect_for<R: Renderer>(
99        &self,
100        id: PinId,
101        text: &str,
102        painter: &Style<'_, R>,
103    ) -> Option<Rect> {
104        self.matching(id)?;
105        let bbox = self.body();
106        Some(
107            crate::render::TagSlot {
108                left: bbox.left(),
109                right: bbox.right(),
110                side: orientation(self.pin),
111                line_y: bbox.center().y,
112            }
113            .bbox(text, painter),
114        )
115    }
116    fn pin_stub_rect(&self, id: PinId) -> Option<Rect> {
117        self.matching(id)?;
118        let bbox = self.body();
119        Some(crate::render::estimate_bbox_for_pin_stub(
120            bbox.left(),
121            bbox.right(),
122            orientation(self.pin),
123            bbox.center().y,
124        ))
125    }
126    fn resizable(&self) -> bool {
127        true
128    }
129}
130
131impl PortShape<'_> {
132    /// The port view's paint pass. Inherent rather than [`BaseShape`]'s
133    /// `render_ng` because it needs the derived accent lookup, which only
134    /// a caller that knows the port's scope can resolve.
135    pub fn render_ng<R: Renderer>(
136        &self,
137        accents: crate::presentation::ShapeAccents<'_>,
138        mode: RenderMode,
139        painter: &mut Style<'_, R>,
140    ) {
141        let pin = self.pin;
142        let bbox = self.body();
143        // The port view faces by `orientation`, not the edge the slot sits on.
144        let side = orientation(pin);
145        let kind = pin.dir;
146        let tag_hidden = pin.tag_hidden;
147        let (name, type_label, tag): (&str, &str, &str) = (&pin.name, &pin.type_name, &pin.tag);
148        // The accent `role` recolors the port outline when it is set; otherwise
149        // it falls back to the default `ShapeStroke`. (The role is ignored when
150        // this pin is rendered as a stub on a block boundary.)
151        let outline = accent_role(accent_from_role(pin.port_accent)).unwrap_or(Role::ShapeStroke);
152        let stub = accent_role(accents.pin(self.id)).unwrap_or(Role::PinStem);
153        let draw_normal = |bbox: Rect, side: PinSide, painter: &mut Style<'_, R>| {
154            crate::render::draw_port(
155                bbox,
156                side,
157                name,
158                type_label,
159                tag,
160                tag_hidden.into(),
161                kind,
162                Role::ShapeFill,
163                (1.0, outline),
164                painter,
165                stub,
166            );
167        };
168
169        match mode {
170            RenderMode::Moving { delta } => {
171                let shifted = bbox.translate(delta);
172                let predicted = snap_rect(shifted);
173                // The port commits to the grid, so draw it at the snapped
174                // position where its route anchor lands; a ghost outline trails
175                // the raw cursor to show the un-snapped drag.
176                crate::render::draw_port_outline(
177                    shifted,
178                    side,
179                    Role::Transparent,
180                    (1.0, Role::DragPreviewStroke),
181                    painter,
182                );
183                crate::render::draw_port_outline(
184                    predicted,
185                    side,
186                    Role::DragActiveFill,
187                    (2.0, Role::DragActiveStroke),
188                    painter,
189                );
190                crate::render::draw_pin_labels(
191                    crate::render::port_text_center(predicted, side),
192                    Align::Center,
193                    name,
194                    type_label,
195                    painter,
196                );
197            }
198            RenderMode::Resizing { mode, delta } => {
199                let resized = resize_rect(&bbox, mode, delta);
200                let predicted = snap_rect(resized);
201                // The port commits to the grid, so draw it at the snapped
202                // position where its route anchor lands; a ghost outline trails
203                // the raw cursor to show the un-snapped resize.
204                crate::render::draw_port_outline(
205                    resized,
206                    side,
207                    Role::Transparent,
208                    (1.0, Role::DragPreviewStroke),
209                    painter,
210                );
211                crate::render::draw_port_outline(
212                    predicted,
213                    side,
214                    Role::DragActiveFill,
215                    (2.0, Role::DragActiveStroke),
216                    painter,
217                );
218                crate::render::draw_pin_labels(
219                    crate::render::port_text_center(predicted, side),
220                    Align::Center,
221                    name,
222                    type_label,
223                    painter,
224                );
225                draw_selection_frame(predicted, Some(mode), painter);
226            }
227            RenderMode::Selected { authoring } => {
228                draw_normal(bbox, side, painter);
229                if authoring == Authoring::Offered {
230                    crate::render::draw_label_placeholders(
231                        crate::render::port_text_center(bbox, side),
232                        Align::Center,
233                        name,
234                        type_label,
235                        painter,
236                    );
237                    if !tag_hidden && tag.is_empty() {
238                        crate::render::draw_tag_placeholder(
239                            bbox.left(),
240                            bbox.right(),
241                            side,
242                            bbox.center().y,
243                            painter,
244                        );
245                    }
246                }
247                draw_selection_frame(bbox, None, painter);
248            }
249            _ => {
250                draw_normal(bbox, side, painter);
251            }
252        }
253    }
254}