Skip to main content

blockworx_tools/
rename_route.rs

1use blockworx_doc::id::{RouteId, RouteLabelId};
2use blockworx_geom::{Align2, Angle, Rect, vec2};
3
4use crate::theme::{Role, Style};
5use crate::{
6    grid::ROUTE_TEXT_SIZE,
7    names::ToolName,
8    tool::{ToolTrait, Transition},
9    widget::drawing::Drawing,
10};
11use blockworx_paint::{Canvas, EditId, EditText, Event, Interaction, Renderer, TextOutcome};
12pub enum RenameRoute {
13    Idle,
14    Renaming {
15        route_id: RouteId,
16        label_id: RouteLabelId,
17        text: String,
18        position: Rect,
19    },
20}
21
22impl RenameRoute {
23    /// Switch to naming `label_id`, or to nothing when the label is not
24    /// where the route says it is.
25    pub fn action(
26        data: &Drawing,
27        route_id: RouteId,
28        label_id: RouteLabelId,
29        painter: &Style<'_, impl Renderer>,
30    ) -> Transition {
31        match RenameRoute::new_with_route_and_label(data, route_id, label_id, painter) {
32            Some(tool) => Transition::SwitchTool(tool.into()),
33            None => Transition::default(),
34        }
35    }
36
37    pub fn new_with_route_and_label(
38        data: &Drawing,
39        route_id: RouteId,
40        label_id: RouteLabelId,
41        painter: &Style<'_, impl Renderer>,
42    ) -> Option<Self> {
43        let wire = data.auto_route(route_id)?;
44        let geometry = data.route_geometry(route_id)?;
45        let name = wire.route.name.clone();
46        let dist = wire
47            .labels
48            .iter()
49            .find(|(id, _)| *id == label_id)
50            .map(|&(_, dist)| dist)?;
51        let label_pos = geometry.map_linear_distance_to_position(dist).location;
52        let editor_width =
53            (painter.text_size(&name, &painter.theme().route_font).x + 10.0).max(60.0);
54        let height = ROUTE_TEXT_SIZE * 1.5;
55        // Sit the editor exactly where the renderer draws a horizontal label:
56        // bottom-center on `horizontal_label_anchor` (a rotated/vertical label
57        // keeps this horizontal editor — only the horizontal case is matched).
58        let anchor = crate::render::horizontal_label_anchor(label_pos);
59        let position =
60            Rect::from_center_size(anchor - vec2(0.0, height / 2.0), vec2(editor_width, height));
61        Some(RenameRoute::Renaming {
62            route_id,
63            label_id,
64            text: name,
65            position,
66        })
67    }
68}
69
70impl ToolTrait for RenameRoute {
71    fn name(&self) -> ToolName {
72        ToolName::RenameRoute
73    }
74
75    fn widget<C: Canvas>(
76        &mut self,
77        data: &mut Drawing,
78        interaction: &Interaction,
79        painter: &mut Style<'_, C>,
80    ) -> Option<Transition> {
81        match self {
82            RenameRoute::Idle => {
83                crate::widget::display::widget(data, interaction, painter);
84                if let Some(Event::DoubleClicked { pos }) = interaction.event
85                    && let Some((route_id, label_id)) = data.route_label_at_pos(pos, painter)
86                    && let Some(tool) =
87                        RenameRoute::new_with_route_and_label(data, route_id, label_id, painter)
88                {
89                    *self = tool;
90                }
91                None
92            }
93            RenameRoute::Renaming {
94                route_id,
95                label_id,
96                text,
97                position,
98            } => {
99                crate::widget::DrawingPasses::new(data)
100                    .suppress_route_label(*route_id, *label_id)
101                    .draw(painter);
102                let id = EditId::of("route_name_edit");
103                let Some(outcome) = &interaction.text else {
104                    painter.set_edit_text(EditText {
105                        align: Align2::CENTER_CENTER,
106                        position: *position,
107                        angle: Angle::ZERO,
108                        text: text.clone(),
109                        font: painter.theme().route_font.clone(),
110                        id,
111                        multiline: false,
112                        char_limit: Some(crate::grid::MAX_LABEL_CHARS),
113                        tab_cycle: false,
114                        // Highlight any existing name so typing replaces it (a
115                        // freshly added label starts empty, where this is a no-op).
116                        select_all_on_focus: true,
117                        hint: Some(crate::render::ADD_ROUTE_LABEL_PLACEHOLDER),
118                        colors: painter
119                            .theme()
120                            .editor_colors(Role::EditorText, Role::EditorFill),
121                        wrap_width: None,
122                    });
123                    return None;
124                };
125                // A cancel writes back the name the editor opened on, which
126                // withdraws a fresh label on a route that has no name to show.
127                let name = match outcome {
128                    TextOutcome::Committed(name) | TextOutcome::Tab(name) => name,
129                    TextOutcome::Cancelled => &*text,
130                };
131                data.set_route_name(*route_id, *label_id, name);
132                // The route stays the standing target for further edits,
133                // its overlay anchored at the label just written.
134                Some(Transition::SwitchTool(
135                    crate::EditRoute::Selected {
136                        id: *route_id,
137                        anchor: position.center(),
138                    }
139                    .into(),
140                ))
141            }
142        }
143    }
144}