paiagram/interface/tabs/
diagram.rs

1use crate::export::ExportObject;
2use crate::graph::Station;
3use crate::interface::SelectedElement;
4use crate::interface::tabs::{Navigatable, Tab};
5use crate::interface::widgets::{buttons, timetable_popup};
6use crate::lines::DisplayedLine;
7use crate::units::time::{Duration, TimetableTime};
8use crate::vehicles::AdjustTimetableEntry;
9use crate::vehicles::entries::ActualRouteEntry;
10use crate::vehicles::entries::{TimetableEntry, TimetableEntryCache, TravelMode};
11use crate::vehicles::vehicle_set::VehicleSet;
12
13use bevy::ecs::entity::{EntityMapper, MapEntities};
14use bevy::ecs::system::RunSystemOnce;
15use bevy::prelude::*;
16use egui::{
17    Color32, CornerRadius, FontId, Frame, Margin, Painter, Popup, Pos2, Rect, RichText, Sense,
18    Shape, Stroke, Ui, UiBuilder, Vec2, response, vec2,
19};
20use egui_i18n::tr;
21use moonshine_core::kind::Instance;
22use serde::{Deserialize, Serialize};
23use strum::EnumCount;
24use strum_macros::EnumCount;
25mod calculate_lines;
26mod edit_line;
27
28// Time and time-canvas related constants
29const TICKS_PER_SECOND: i64 = 100;
30
31// TODO: implement multi select and editing
32#[derive(PartialEq, Eq, Clone, Copy)]
33#[allow(dead_code)]
34pub enum SelectedEntityType {
35    Vehicle(Entity),
36    TimetableEntry { entry: Entity, vehicle: Entity },
37    Interval((Instance<Station>, Instance<Station>)),
38    Station(Instance<Station>),
39    Map(Entity),
40}
41
42#[derive(Debug, Clone)]
43pub struct DiagramPageCache {
44    /// The previous total drag delta, used for dragging time points on the canvas
45    previous_total_drag_delta: Option<f32>,
46    /// The stroke style used for drawing lines on the diagram
47    /// TODO: make this adapt to dark and light mode, and train settings
48    stroke: Stroke,
49    /// Horizontal tick offset for panning
50    tick_offset: i64,
51    /// Vertical offset for panning
52    vertical_offset: f32,
53    zoom: Vec2,
54    line_cache: DiagramLineCache,
55}
56
57impl MapEntities for DiagramPageCache {
58    fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
59        if let Some(heights) = &mut self.line_cache.heights {
60            for (station, _) in heights.iter_mut() {
61                station.map_entities(entity_mapper);
62            }
63        }
64        for entity in &mut self.line_cache.vehicle_entities {
65            entity.map_entities(entity_mapper);
66        }
67        if let Some(vehicle_set) = &mut self.line_cache.vehicle_set {
68            vehicle_set.map_entities(entity_mapper);
69        }
70    }
71}
72
73impl DiagramPageCache {
74    // linear search is quicker for a small data set
75    fn get_visible_stations(&self, range: std::ops::Range<f32>) -> &[(Instance<Station>, f32)] {
76        let Some(heights) = &self.line_cache.heights else {
77            return &[];
78        };
79        let first_visible = heights.iter().position(|(_, h)| *h > range.start);
80        let last_visible = heights.iter().rposition(|(_, h)| *h < range.end);
81        if let (Some(mut first_visible), Some(mut last_visible)) = (first_visible, last_visible) {
82            // saturating sub 2 to add some buffer
83            first_visible = first_visible.saturating_sub(2);
84            last_visible = (last_visible + 1).min(heights.len() - 1);
85            &heights[first_visible..=last_visible]
86        } else {
87            &[]
88        }
89    }
90}
91
92impl Default for DiagramPageCache {
93    fn default() -> Self {
94        Self {
95            previous_total_drag_delta: None,
96            tick_offset: 0,
97            vertical_offset: 0.0,
98            stroke: Stroke {
99                width: 1.0,
100                color: Color32::BLACK,
101            },
102            zoom: vec2(0.0005, 1.0),
103            line_cache: DiagramLineCache::default(),
104        }
105    }
106}
107
108#[derive(Debug, Clone, Default)]
109struct DiagramLineCache {
110    heights: Option<Vec<(Instance<Station>, f32)>>,
111    vehicle_entities: Vec<Entity>,
112    vehicle_set: Option<Entity>,
113    line_missing: bool,
114    last_render_context: Option<DiagramRenderContext>,
115}
116
117#[derive(Debug, Clone)]
118struct DiagramRenderContext {
119    screen_rect: Rect,
120    vertical_visible: std::ops::Range<f32>,
121    horizontal_visible: std::ops::Range<i64>,
122    ticks_per_screen_unit: f64,
123}
124
125#[derive(Debug, Clone)]
126struct DiagramLineParams {
127    tick_offset: i64,
128    vertical_offset: f32,
129    zoom_y: f32,
130    stroke: Stroke,
131}
132
133type PointData = (Pos2, Option<Pos2>, ActualRouteEntry);
134
135#[derive(Debug, Clone)]
136struct RenderedVehicle {
137    segments: Vec<Vec<PointData>>,
138    stroke: Stroke,
139    entity: Entity,
140}
141
142#[derive(PartialEq, Debug, Default, Clone, EnumCount, Serialize, Deserialize)]
143enum EditingState {
144    #[default]
145    None,
146    EditingLine,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct DiagramTab {
151    pub displayed_line_entity: Entity,
152    editing: EditingState,
153    #[serde(skip, default)]
154    state: DiagramPageCache,
155    #[serde(skip)]
156    typst_output: String,
157    #[serde(skip)]
158    rendered_vehicles_cache: Vec<RenderedVehicle>,
159}
160
161impl MapEntities for DiagramTab {
162    fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
163        self.displayed_line_entity.map_entities(entity_mapper);
164        self.state.map_entities(entity_mapper);
165    }
166}
167
168impl DiagramTab {
169    pub fn new(displayed_line_entity: Entity) -> Self {
170        Self {
171            displayed_line_entity,
172            editing: EditingState::default(),
173            state: DiagramPageCache::default(),
174            typst_output: String::new(),
175            rendered_vehicles_cache: Vec::new(),
176        }
177    }
178}
179
180impl Navigatable for DiagramTab {
181    fn zoom_x(&self) -> f32 {
182        self.state.zoom.x
183    }
184
185    fn zoom_y(&self) -> f32 {
186        self.state.zoom.y
187    }
188
189    fn set_zoom(&mut self, zoom_x: f32, zoom_y: f32) {
190        self.state.zoom = vec2(zoom_x, zoom_y);
191    }
192
193    fn offset_x(&self) -> f64 {
194        self.state.tick_offset as f64
195    }
196
197    fn offset_y(&self) -> f32 {
198        self.state.vertical_offset
199    }
200
201    fn set_offset(&mut self, offset_x: f64, offset_y: f32) {
202        self.state.tick_offset = offset_x.round() as i64;
203        self.state.vertical_offset = offset_y;
204    }
205
206    fn allow_axis_zoom(&self) -> bool {
207        true
208    }
209
210    fn clamp_zoom(&self, zoom_x: f32, zoom_y: f32) -> (f32, f32) {
211        (zoom_x.clamp(0.00001, 0.4), zoom_y.clamp(0.025, 2048.0))
212    }
213
214    fn post_navigation(&mut self, response: &egui::Response) {
215        self.state.tick_offset = self.state.tick_offset.clamp(
216            -366 * 86400 * TICKS_PER_SECOND,
217            366 * 86400 * TICKS_PER_SECOND
218                - (response.rect.width() as f64 / self.state.zoom.x as f64) as i64,
219        );
220        const TOP_BOTTOM_PADDING: f32 = 30.0;
221        let max_height = self
222            .state
223            .line_cache
224            .heights
225            .as_ref()
226            .and_then(|h| h.last().map(|(_, h)| *h))
227            .unwrap_or(0.0);
228        self.state.vertical_offset = if response.rect.height() / self.state.zoom.y
229            > (max_height + TOP_BOTTOM_PADDING * 2.0 / self.state.zoom.y)
230        {
231            (-response.rect.height() / self.state.zoom.y + max_height) / 2.0
232        } else {
233            self.state.vertical_offset.clamp(
234                -TOP_BOTTOM_PADDING / self.state.zoom.y,
235                max_height - response.rect.height() / self.state.zoom.y
236                    + TOP_BOTTOM_PADDING / self.state.zoom.y,
237            )
238        }
239    }
240}
241
242impl PartialEq for DiagramTab {
243    fn eq(&self, other: &Self) -> bool {
244        self.displayed_line_entity == other.displayed_line_entity
245    }
246}
247
248impl Eq for DiagramTab {}
249
250impl Tab for DiagramTab {
251    const NAME: &'static str = "Diagram";
252    fn pre_render(&mut self, world: &mut World) {}
253    fn post_render(&mut self, _world: &mut World) {
254        self.rendered_vehicles_cache.clear();
255    }
256    fn title(&self) -> egui::WidgetText {
257        tr!("tab-diagram").into()
258    }
259    fn main_display(&mut self, world: &mut World, ui: &mut Ui) {
260        Frame::canvas(ui.style())
261            .inner_margin(Margin::ZERO)
262            .show(ui, |ui| {
263                self.state.stroke.color = ui.visuals().text_color();
264
265                let (response, mut painter) =
266                    ui.allocate_painter(ui.available_size_before_wrap(), Sense::click_and_drag());
267
268                self.handle_navigation(ui, &response);
269
270                let (vertical_visible, horizontal_visible, ticks_per_screen_unit) =
271                    calculate_visible_ranges(&self.state, &response.rect);
272
273                self.state.line_cache.last_render_context = Some(DiagramRenderContext {
274                    screen_rect: response.rect,
275                    vertical_visible: vertical_visible.clone(),
276                    horizontal_visible: horizontal_visible.clone(),
277                    ticks_per_screen_unit,
278                });
279
280                let line_params = DiagramLineParams {
281                    tick_offset: self.state.tick_offset,
282                    vertical_offset: self.state.vertical_offset,
283                    zoom_y: self.state.zoom.y,
284                    stroke: self.state.stroke.clone(),
285                };
286                if let Err(e) = world.run_system_cached_with(
287                    calculate_lines::calculate_lines,
288                    (
289                        self.displayed_line_entity,
290                        &mut self.state.line_cache,
291                        &mut self.rendered_vehicles_cache,
292                        line_params,
293                    ),
294                ) {
295                    error!("Error calculating lines for diagram: {}", e);
296                }
297
298                if let Err(e) = world.run_system_cached_with(
299                    show_diagram,
300                    (
301                        ui,
302                        &mut self.state,
303                        &self.rendered_vehicles_cache,
304                        &mut painter,
305                        &response,
306                    ),
307                ) {
308                    error!(
309                        "UI Error while displaying diagram ({}): {}",
310                        self.displayed_line_entity, e
311                    )
312                }
313            });
314    }
315    fn edit_display(&mut self, world: &mut World, ui: &mut Ui) {
316        ui.group(|ui| {
317            world.run_system_cached_with(
318                select_vehicle_set,
319                (ui, &mut self.state.line_cache.vehicle_set),
320            );
321            if ui
322                .button("Generate intervals from displayed line")
323                .clicked()
324                && let Err(e) = world.run_system_once_with(
325                    crate::lines::create_intervals_from_displayed_line,
326                    self.displayed_line_entity,
327                )
328            {
329                error!(
330                    "Error while generating intervals from displayed line: {:?}",
331                    e
332                )
333            }
334            if ui
335                .button("Automatically adjust intervals' length")
336                .clicked()
337                && let Err(e) = world.run_system_once_with(
338                    crate::lines::adjust_intervals_length,
339                    self.displayed_line_entity,
340                )
341            {
342                error!("Error while automatically adjusting intervals: {:?}", e)
343            }
344        });
345        // edit line, edit stations on line, etc.
346        let width = ui.available_width();
347        let spacing = ui.spacing().item_spacing.x;
348        let element_width = (width - spacing) / EditingState::COUNT as f32;
349        ui.horizontal(|ui| {
350            if ui
351                .add_sized(
352                    [element_width, 30.0],
353                    egui::Button::new("None").selected(self.editing == EditingState::None),
354                )
355                .clicked()
356            {
357                self.editing = EditingState::None;
358            }
359            if ui
360                .add_sized(
361                    [element_width, 30.0],
362                    egui::Button::new("Edit Lines")
363                        .selected(self.editing == EditingState::EditingLine),
364                )
365                .clicked()
366            {
367                self.editing = EditingState::EditingLine;
368            }
369        });
370        // match current_tab.0 {
371        //     // There are nothing selected. In this case, provide tools for editing the displayed line itself
372        //     // and the vehicles on it.
373        //     None => {}
374        //     _ => {}
375        // }
376        if self.editing == EditingState::EditingLine {
377            egui::ScrollArea::vertical().show(ui, |ui| {
378                world.run_system_cached_with(edit_line::edit_line, (ui, self.displayed_line_entity))
379            });
380        }
381    }
382    fn display_display(&mut self, world: &mut World, ui: &mut Ui) {
383        let current_tab = world.resource::<SelectedElement>();
384        use super::super::side_panel::*;
385        match current_tab.0 {
386            None => {
387                // this is technically oblique, but let's just wait until the next version of egui.
388                ui.label(RichText::new("Nothing Selected").italics());
389            }
390            Some(SelectedEntityType::Interval(i)) => {
391                if let Err(e) =
392                    world.run_system_cached_with(interval_stats::show_interval_stats, (ui, i))
393                {
394                    error!("UI Error while displaying interval stats: {}", e);
395                }
396            }
397            Some(SelectedEntityType::Map(_)) => {}
398            Some(SelectedEntityType::Station(s)) => {
399                if let Err(e) =
400                    world.run_system_cached_with(station_stats::show_station_stats, (ui, s))
401                {
402                    error!("UI Error while displaying station stats: {}", e);
403                }
404            }
405            Some(SelectedEntityType::TimetableEntry {
406                entry: _,
407                vehicle: _,
408            }) => {}
409            Some(SelectedEntityType::Vehicle(v)) => {
410                if let Err(e) =
411                    world.run_system_cached_with(vehicle_stats::show_vehicle_stats, (ui, v))
412                {
413                    error!("UI Error while displaying vehicle stats: {}", e);
414                }
415            }
416        }
417    }
418    fn export_display(&mut self, world: &mut World, ui: &mut Ui) {
419        ui.group(|ui| {
420            ui.strong(tr!("tab-diagram-export-typst-diagram"));
421            ui.label(tr!("tab-diagram-export-typst-diagram-desc"));
422            // TODO: make the export range configurable
423            if ui.button(tr!("export")).clicked() {
424                let mut calculated_vehicles: Vec<RenderedVehicle> = Vec::new();
425                let mut line_cache = self.state.line_cache.clone();
426                let max_height = line_cache
427                    .heights
428                    .as_ref()
429                    .and_then(|h| h.last().map(|(_, h)| *h))
430                    .unwrap_or(0.0);
431                let ticks_per_screen_unit = 1.0 / self.state.zoom.x as f64;
432                let horizontal_visible = 0..86400 * TICKS_PER_SECOND;
433                let vertical_visible = 0.0..max_height;
434                let width = (horizontal_visible.end - horizontal_visible.start) as f64
435                    / ticks_per_screen_unit;
436                let height = max_height * self.state.zoom.y;
437                line_cache.last_render_context = Some(DiagramRenderContext {
438                    screen_rect: Rect::from_min_size(Pos2::ZERO, vec2(width as f32, height)),
439                    vertical_visible,
440                    horizontal_visible,
441                    ticks_per_screen_unit,
442                });
443                if let Err(e) = world.run_system_cached_with(
444                    calculate_lines::calculate_lines,
445                    (
446                        self.displayed_line_entity,
447                        &mut line_cache,
448                        &mut calculated_vehicles,
449                        DiagramLineParams {
450                            tick_offset: 0,
451                            vertical_offset: 0.0,
452                            zoom_y: self.state.zoom.y,
453                            stroke: self.state.stroke.clone(),
454                        },
455                    ),
456                ) {
457                    error!("Error calculating lines for diagram: {}", e);
458                }
459                if let Err(e) = world.run_system_once_with(
460                    make_typst_string,
461                    (
462                        &mut self.typst_output,
463                        &calculated_vehicles,
464                        width as f32,
465                        line_cache
466                            .heights
467                            .as_ref()
468                            .map(|h| h.iter().map(|(_, height)| *height).collect::<Vec<_>>())
469                            .as_deref()
470                            .unwrap_or(&[]),
471                    ),
472                ) {
473                    error!("UI Error while exporting diagram to typst: {}", e);
474                }
475            }
476            if ui.button(tr!("copy-to-clipboard")).clicked() {
477                ui.ctx().copy_text(self.typst_output.clone());
478            }
479            ui.label(tr!("tab-diagram-export-typst-diagram-output", {
480                bytes: self.typst_output.len()
481            }));
482        });
483        ui.strong(tr!("tab-diagram-export-typst-timetable"));
484        ui.strong(tr!("tab-diagram-export-typst-timetable-desc"));
485        if ui.button("Export").clicked()
486            && let Err(e) = crate::export::typst_timetable::TypstTimetable.export_to_file(
487                world,
488                (
489                    self.state.line_cache.vehicle_entities.iter().cloned(),
490                    self.displayed_line_entity,
491                ),
492            )
493        {
494            error!("Error exporting typst timetable: {:?}", e)
495        }
496    }
497    fn id(&self) -> egui::Id {
498        egui::Id::new(self.displayed_line_entity)
499    }
500    fn scroll_bars(&self) -> [bool; 2] {
501        [false; 2]
502    }
503    fn frame(&self) -> egui::Frame {
504        egui::Frame::default().inner_margin(egui::Margin::same(2))
505    }
506}
507
508fn make_typst_string(
509    (InMut(buffer), InRef(calculated_vehicles), In(width), InRef(heights)): (
510        InMut<String>,
511        InRef<[RenderedVehicle]>,
512        In<f32>,
513        InRef<[f32]>,
514    ),
515) {
516    buffer.clear();
517    buffer.push_str(&format!(
518        r#"#set page(width: auto, height: auto)
519#let render-diagram(
520  segments,
521  width: {width}pt,
522  heights: ({}),
523  horizontal_scale: 1,
524  vertical_scale: 1,
525) = box({{
526  for segment in segments {{
527    let (first, ..rest) = segment
528    let first = curve.move((first.at(0) * 1pt * horizontal_scale, first.at(1) * 1pt * vertical_scale))
529    let a = rest.map(((x, y)) => curve.line((x * 1pt * horizontal_scale, y * 1pt * vertical_scale)))
530    place(curve(first, ..a))
531  }}
532  grid(
533    columns: (width * horizontal_scale / 24,) * 24,
534    rows: {{
535      let heights = heights.map(h => h * vertical_scale)
536      let (_, a) = heights.fold((0pt, (0pt,)), ((curr, acc), v) => (v, acc + (v - curr,)))
537      a
538    }},
539    stroke: 1pt,
540  )
541}})
542
543#let segments = (
544"#, heights.iter().map(|h| format!("{}pt", h)).collect::<Vec<_>>().join(", "))
545    );
546    for calculated_vehicle in calculated_vehicles {
547        for segment in calculated_vehicle.segments.iter().map(|s| {
548            s.iter()
549                .flat_map(|(a_pos, d_pos, _entry)| std::iter::once(a_pos).chain(d_pos.iter()))
550        }) {
551            buffer.push_str("  (\n");
552            for point in segment {
553                buffer.push_str(&format!("    ({}, {}),\n", point.x, point.y));
554            }
555            buffer.push_str("  ),\n");
556        }
557    }
558    buffer.push_str("\n)\n#render-diagram(segments)");
559}
560
561fn select_vehicle_set(
562    (InMut(ui), InMut(vehicle_set)): (InMut<egui::Ui>, InMut<Option<Entity>>),
563    vehicle_sets: Query<(Entity, &Name), With<VehicleSet>>,
564) {
565    let displayed_text = vehicle_set.map_or("None", |e| {
566        vehicle_sets
567            .get(e)
568            .map(|(_, name)| name.as_str())
569            .unwrap_or("Unknown")
570    });
571    egui::ComboBox::from_id_salt("vehicle set")
572        .selected_text(displayed_text)
573        .show_ui(ui, |ui| {
574            for (entity, name) in vehicle_sets {
575                ui.selectable_value(vehicle_set, Some(entity), name.as_str());
576            }
577        });
578}
579
580fn show_diagram(
581    (InMut(ui), InMut(state), InRef(rendered_vehicles), InMut(mut painter), InRef(response)): (
582        InMut<egui::Ui>,
583        InMut<DiagramPageCache>,
584        InRef<[RenderedVehicle]>,
585        InMut<Painter>,
586        InRef<response::Response>,
587    ),
588    station_names: Query<&Name, With<Station>>,
589    timetable_entries: Query<(&TimetableEntry, &TimetableEntryCache)>,
590    mut selected_element: ResMut<SelectedElement>,
591    mut timetable_adjustment_writer: MessageWriter<AdjustTimetableEntry>,
592    // Buffer used between all calls to avoid repeated allocations
593    mut visible_stations_scratch: Local<Vec<(Instance<Station>, f32)>>,
594) {
595    if state.line_cache.line_missing {
596        ui.centered_and_justified(|ui| ui.heading("Diagram not found"));
597        return;
598    };
599
600    ui.style_mut().visuals.menu_corner_radius = CornerRadius::ZERO;
601    ui.style_mut().visuals.window_stroke.width = 0.0;
602
603    let Some(render_context) = state.line_cache.last_render_context.clone() else {
604        return;
605    };
606
607    // `get_visible_stations` returns a slice borrowed from `state`, so copy it out
608    // (into a reusable buffer) to avoid holding an immutable borrow of `state`
609    // across later mutations.
610    visible_stations_scratch.clear();
611    visible_stations_scratch
612        .extend_from_slice(state.get_visible_stations(render_context.vertical_visible.clone()));
613    let visible_stations: &[(Instance<Station>, f32)] = visible_stations_scratch.as_slice();
614
615    draw_station_lines(
616        state.vertical_offset,
617        state.stroke,
618        &mut painter,
619        &render_context.screen_rect,
620        state.zoom.y,
621        visible_stations,
622        ui.pixels_per_point(),
623        ui.visuals().text_color(),
624        |e| station_names.get(e).ok().map(|s| s.as_str()),
625    );
626
627    draw_time_lines(
628        state.tick_offset,
629        state.stroke,
630        &mut painter,
631        &render_context.screen_rect,
632        render_context.ticks_per_screen_unit,
633        &render_context.horizontal_visible,
634        ui.pixels_per_point(),
635    );
636
637    if response.clicked()
638        && let Some(pos) = response.interact_pointer_pos()
639    {
640        handle_input_selection(
641            pos,
642            rendered_vehicles,
643            visible_stations,
644            &render_context.screen_rect,
645            state.vertical_offset,
646            state.zoom.y,
647            &mut selected_element,
648        );
649    }
650
651    let background_strength = ui.ctx().animate_bool(
652        ui.id().with("background animation"),
653        selected_element.is_some(),
654    );
655
656    draw_vehicles(&mut painter, rendered_vehicles, &mut selected_element);
657
658    if background_strength > 0.1 {
659        painter.rect_filled(painter.clip_rect(), CornerRadius::ZERO, {
660            let amt = (background_strength * 180.0) as u8;
661            if ui.ctx().theme().default_visuals().dark_mode {
662                Color32::from_black_alpha(amt)
663            } else {
664                Color32::from_white_alpha(amt)
665            }
666        });
667    }
668
669    let show_button = state.zoom.x.min(state.zoom.y) > 0.0002;
670    let button_strength = ui
671        .ctx()
672        .animate_bool(ui.id().with("all buttons animation"), show_button);
673
674    match selected_element.0 {
675        None => {}
676        Some(SelectedEntityType::Vehicle(v)) => {
677            draw_vehicle_selection_overlay(
678                ui,
679                &mut painter,
680                &rendered_vehicles,
681                state,
682                background_strength,
683                button_strength,
684                v,
685                &mut timetable_adjustment_writer,
686                &station_names,
687                &timetable_entries,
688            );
689        }
690        Some(SelectedEntityType::TimetableEntry {
691            entry: _,
692            vehicle: _,
693        }) => {}
694        Some(SelectedEntityType::Interval(i)) => {
695            draw_interval_selection_overlay(
696                ui,
697                background_strength,
698                &mut painter,
699                render_context.screen_rect,
700                state.vertical_offset,
701                state.zoom.y,
702                i,
703                visible_stations,
704            );
705        }
706        Some(SelectedEntityType::Station(s)) => {
707            draw_station_selection_overlay(
708                ui,
709                background_strength,
710                &mut painter,
711                render_context.screen_rect,
712                state.vertical_offset,
713                state.zoom.y,
714                s,
715                visible_stations,
716            );
717        }
718        Some(SelectedEntityType::Map(_)) => {
719            todo!("Do this bro")
720        }
721    }
722}
723
724fn ensure_heights(line_cache: &mut DiagramLineCache, displayed_line: &DisplayedLine) {
725    let mut current_height = 0.0;
726    let mut heights = Vec::new();
727    for (station, distance) in displayed_line.stations() {
728        current_height += distance.abs().log2().max(1.0) * 15f32;
729        heights.push((*station, current_height))
730    }
731    line_cache.heights = Some(heights);
732}
733
734fn calculate_visible_ranges(
735    state: &DiagramPageCache,
736    rect: &Rect,
737) -> (std::ops::Range<f32>, std::ops::Range<i64>, f64) {
738    let vertical_visible =
739        state.vertical_offset..rect.height() / state.zoom.y + state.vertical_offset;
740    let horizontal_visible =
741        state.tick_offset..state.tick_offset + (rect.width() as f64 / state.zoom.x as f64) as i64;
742    let ticks_per_screen_unit =
743        (horizontal_visible.end - horizontal_visible.start) as f64 / rect.width() as f64;
744    (vertical_visible, horizontal_visible, ticks_per_screen_unit)
745}
746
747fn handle_input_selection(
748    pointer_pos: Pos2,
749    rendered_vehicles: &[RenderedVehicle],
750    visible_stations: &[(Instance<Station>, f32)],
751    screen_rect: &Rect,
752    vertical_offset: f32,
753    zoom_y: f32,
754    selected_entity: &mut Option<SelectedEntityType>,
755) {
756    const VEHICLE_SELECTION_RADIUS: f32 = 7.0;
757    const STATION_SELECTION_RADIUS: f32 = VEHICLE_SELECTION_RADIUS;
758    if selected_entity.is_some() {
759        *selected_entity = None;
760        return;
761    };
762    let mut found: Option<SelectedEntityType> = None;
763    'check_selected: for vehicle in rendered_vehicles {
764        for segment in &vehicle.segments {
765            let mut points = segment
766                .iter()
767                .flat_map(|(a_pos, d_pos, ..)| std::iter::once(*a_pos).chain(*d_pos));
768
769            if let Some(mut curr) = points.next() {
770                for next in points {
771                    let a = pointer_pos.x - curr.x;
772                    let b = pointer_pos.y - curr.y;
773                    let c = next.x - curr.x;
774                    let d = next.y - curr.y;
775                    let dot = a * c + b * d;
776                    let len_sq = c * c + d * d;
777                    if len_sq == 0.0 {
778                        continue;
779                    }
780                    let t = (dot / len_sq).clamp(0.0, 1.0);
781                    let px = curr.x + t * c;
782                    let py = curr.y + t * d;
783                    let dx = pointer_pos.x - px;
784                    let dy = pointer_pos.y - py;
785
786                    if dx * dx + dy * dy < VEHICLE_SELECTION_RADIUS.powi(2) {
787                        found = Some(SelectedEntityType::Vehicle(vehicle.entity));
788                        break 'check_selected;
789                    }
790                    curr = next;
791                }
792            }
793        }
794    }
795    if found.is_some() {
796        *selected_entity = found;
797        return;
798    }
799    // Handle station lines after vehicle lines,
800    for (station_entity, height) in visible_stations {
801        let y = (*height - vertical_offset) * zoom_y + screen_rect.top();
802        if (y - STATION_SELECTION_RADIUS..y + STATION_SELECTION_RADIUS).contains(&pointer_pos.y) {
803            found = Some(SelectedEntityType::Station(*station_entity));
804            break;
805        }
806    }
807    if found.is_some() {
808        *selected_entity = found;
809        return;
810    }
811    for w in visible_stations.windows(2) {
812        let [(e1, h1), (e2, h2)] = w else {
813            continue;
814        };
815        let y1 = (*h1 - vertical_offset) * zoom_y + screen_rect.top();
816        let y2 = (*h2 - vertical_offset) * zoom_y + screen_rect.top();
817        let (min_y, max_y) = if y1 <= y2 { (y1, y2) } else { (y2, y1) };
818        if (min_y..max_y).contains(&pointer_pos.y) {
819            found = Some(SelectedEntityType::Interval((*e1, *e2)));
820            break;
821        }
822    }
823    if found.is_some() {
824        *selected_entity = found;
825        return;
826    }
827}
828
829fn draw_vehicles(
830    painter: &mut Painter,
831    rendered_vehicles: &[RenderedVehicle],
832    selected_entity: &Option<SelectedEntityType>,
833) {
834    let mut selected_vehicle = None;
835    for vehicle in rendered_vehicles {
836        if selected_vehicle.is_none()
837            && let Some(selected_entity) = selected_entity
838            && matches!(selected_entity, SelectedEntityType::Vehicle(e) if *e == vehicle.entity)
839        {
840            selected_vehicle = Some(vehicle);
841            continue;
842        }
843
844        for segment in &vehicle.segments {
845            let points = segment
846                .iter()
847                .flat_map(|(a, d, ..)| std::iter::once(*a).chain(*d))
848                .collect::<Vec<_>>();
849            painter.line(points, vehicle.stroke);
850        }
851    }
852}
853
854fn draw_vehicle_selection_overlay(
855    ui: &mut Ui,
856    painter: &mut Painter,
857    rendered_vehicles: &[RenderedVehicle],
858    state: &mut DiagramPageCache,
859    line_strength: f32,
860    button_strength: f32,
861    selected_entity: Entity,
862    timetable_adjustment_writer: &mut MessageWriter<AdjustTimetableEntry>,
863    station_names: &Query<&Name, With<Station>>,
864    timetable_entries: &Query<(&TimetableEntry, &TimetableEntryCache)>,
865) {
866    let Some(vehicle) = rendered_vehicles
867        .iter()
868        .find(|v| selected_entity == v.entity)
869    else {
870        return;
871    };
872
873    let mut stroke = vehicle.stroke;
874    stroke.width = line_strength * 3.0 * stroke.width + stroke.width;
875
876    for (line_index, segment) in vehicle.segments.iter().enumerate() {
877        let mut line_vec = Vec::with_capacity(segment.len() * 2);
878
879        for idx in 0..segment.len().saturating_sub(1) {
880            let (arrival_pos, departure_pos, entry_entity) = segment[idx];
881            let (next_arrival_pos, _, next_entry_entity) = segment[idx + 1];
882            let Ok((_, entry_cache)) = timetable_entries.get(entry_entity.inner()) else {
883                continue;
884            };
885            let Ok((next_entry, next_entry_cache)) =
886                timetable_entries.get(next_entry_entity.inner())
887            else {
888                continue;
889            };
890            let signal_stroke = Stroke {
891                width: 1.0 + line_strength,
892                color: if matches!(next_entry.arrival, TravelMode::For(_)) {
893                    Color32::BLUE
894                } else {
895                    Color32::ORANGE
896                },
897            };
898
899            line_vec.push(arrival_pos);
900            let mut curr_pos = if let Some(d_pos) = departure_pos {
901                line_vec.push(d_pos);
902                d_pos
903            } else {
904                arrival_pos
905            };
906
907            let mut next_pos = next_arrival_pos;
908
909            curr_pos.y += 5.0;
910            next_pos.y += 5.0;
911
912            signal_stroke.round_center_to_pixel(ui.pixels_per_point(), &mut curr_pos.x);
913            signal_stroke.round_center_to_pixel(ui.pixels_per_point(), &mut curr_pos.y);
914            signal_stroke.round_center_to_pixel(ui.pixels_per_point(), &mut next_pos.x);
915            signal_stroke.round_center_to_pixel(ui.pixels_per_point(), &mut next_pos.y);
916
917            let duration = next_entry_cache.estimate.as_ref().unwrap().arrival
918                - entry_cache.estimate.as_ref().unwrap().departure;
919
920            let points = if next_pos.y <= curr_pos.y {
921                vec![curr_pos, Pos2::new(next_pos.x, curr_pos.y), next_pos]
922            } else {
923                vec![curr_pos, Pos2::new(curr_pos.x, next_pos.y), next_pos]
924            };
925            painter.add(Shape::line(points, signal_stroke));
926
927            if duration != Duration(0) {
928                let time = duration.to_hms();
929                let text = format!("{}:{:02}", time.0 * 60 + time.1, time.2);
930                let duration_text = painter.layout_no_wrap(
931                    text,
932                    egui::FontId::monospace(15.0),
933                    signal_stroke.color,
934                );
935                painter.galley(
936                    Pos2 {
937                        x: (curr_pos.x + next_pos.x - duration_text.size().x) / 2.0,
938                        y: curr_pos.y.max(next_pos.y) + 1.0,
939                    },
940                    duration_text,
941                    signal_stroke.color,
942                );
943            }
944        }
945
946        if let Some(last_pos) = segment.last() {
947            line_vec.push(last_pos.0);
948            if let Some(departure) = last_pos.1 {
949                line_vec.push(departure)
950            }
951        }
952        painter.line(line_vec, stroke);
953
954        let mut previous_entry: Option<ActualRouteEntry> = None;
955        if button_strength <= 0.0 {
956            continue;
957        }
958        for fragment in segment
959            .iter()
960            .copied()
961            .filter(|(_, _, a)| matches!(a, ActualRouteEntry::Nominal(_)))
962        {
963            let (mut arrival_pos, maybe_departure_pos, entry_entity) = fragment;
964            let Ok((entry, entry_cache)) = timetable_entries.get(entry_entity.inner()) else {
965                continue;
966            };
967            const HANDLE_SIZE: f32 = 12.0;
968            const CIRCLE_HANDLE_SIZE: f32 = 7.0;
969            const TRIANGLE_HANDLE_SIZE: f32 = 10.0;
970            const DASH_HANDLE_SIZE: f32 = 9.0;
971            let departure_pos: Pos2;
972            if let Some(unwrapped_pos) = maybe_departure_pos {
973                if (arrival_pos.x - unwrapped_pos.x).abs() < HANDLE_SIZE {
974                    let midpoint_x = (arrival_pos.x + unwrapped_pos.x) / 2.0;
975                    arrival_pos.x = midpoint_x - HANDLE_SIZE / 2.0;
976                    let mut pos = unwrapped_pos;
977                    pos.x = midpoint_x + HANDLE_SIZE / 2.0;
978                    departure_pos = pos;
979                } else {
980                    departure_pos = unwrapped_pos;
981                }
982            } else {
983                arrival_pos.x -= HANDLE_SIZE / 2.0;
984                let mut pos = arrival_pos;
985                pos.x += HANDLE_SIZE;
986                departure_pos = pos;
987            };
988            let arrival_point_response =
989                ui.place(Rect::from_pos(arrival_pos).expand(5.2), |ui: &mut Ui| {
990                    let (rect, resp) =
991                        ui.allocate_exact_size(ui.available_size(), Sense::click_and_drag());
992                    ui.scope_builder(
993                        UiBuilder::new()
994                            .sense(resp.sense)
995                            .max_rect(rect)
996                            .id(ui.id().with(
997                                entry_entity.inner().to_bits() as u128 | (line_index as u128) << 64,
998                            )),
999                        |ui| {
1000                            ui.set_min_size(ui.available_size());
1001                            let response = ui.response();
1002                            let fill = if response.hovered() {
1003                                Color32::GRAY
1004                            } else {
1005                                Color32::WHITE
1006                            }
1007                            .linear_multiply(button_strength);
1008                            let handle_stroke = Stroke {
1009                                width: 2.5,
1010                                color: stroke.color.linear_multiply(button_strength),
1011                            };
1012                            match entry.arrival {
1013                                TravelMode::At(_) => buttons::circle_button_shape(
1014                                    painter,
1015                                    arrival_pos,
1016                                    CIRCLE_HANDLE_SIZE,
1017                                    handle_stroke,
1018                                    fill,
1019                                ),
1020                                TravelMode::For(_) => buttons::dash_button_shape(
1021                                    painter,
1022                                    arrival_pos,
1023                                    DASH_HANDLE_SIZE,
1024                                    handle_stroke,
1025                                    fill,
1026                                ),
1027                                TravelMode::Flexible => buttons::triangle_button_shape(
1028                                    painter,
1029                                    arrival_pos,
1030                                    TRIANGLE_HANDLE_SIZE,
1031                                    handle_stroke,
1032                                    fill,
1033                                ),
1034                            };
1035                        },
1036                    )
1037                    .response
1038                });
1039
1040            if arrival_point_response.drag_started() {
1041                state.previous_total_drag_delta = None;
1042            }
1043            if let Some(total_drag_delta) = arrival_point_response.total_drag_delta() {
1044                let previous_drag_delta = state.previous_total_drag_delta.unwrap_or(0.0);
1045                let duration = Duration(
1046                    ((total_drag_delta.x as f64 - previous_drag_delta as f64)
1047                        / state.zoom.x as f64
1048                        / TICKS_PER_SECOND as f64) as i32,
1049                );
1050                if duration != Duration(0) {
1051                    timetable_adjustment_writer.write(AdjustTimetableEntry {
1052                        entity: entry_entity.inner(),
1053                        adjustment: crate::vehicles::TimetableAdjustment::AdjustArrivalTime(
1054                            duration,
1055                        ),
1056                    });
1057                    state.previous_total_drag_delta = Some(
1058                        previous_drag_delta
1059                            + (duration.0 as f64 * TICKS_PER_SECOND as f64 * state.zoom.x as f64)
1060                                as f32,
1061                    );
1062                }
1063            }
1064            if arrival_point_response.drag_stopped() {
1065                state.previous_total_drag_delta = None;
1066            }
1067            if arrival_point_response.dragged() {
1068                arrival_point_response.show_tooltip_ui(|ui| {
1069                    ui.label(entry_cache.estimate.as_ref().unwrap().arrival.to_string());
1070                    ui.label(
1071                        station_names
1072                            .get(entry.station.entity())
1073                            .map_or("??", |s| s.as_str()),
1074                    );
1075                });
1076            } else {
1077                Popup::menu(&arrival_point_response)
1078                    .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
1079                    .show(|ui| {
1080                        timetable_popup::popup(
1081                            entry_entity.inner(),
1082                            (entry, entry_cache),
1083                            previous_entry.and_then(|e| timetable_entries.get(e.inner()).ok()),
1084                            timetable_adjustment_writer,
1085                            ui,
1086                            true,
1087                        );
1088                    });
1089            }
1090
1091            let departure_point_response =
1092                ui.put(Rect::from_pos(departure_pos).expand(4.5), |ui: &mut Ui| {
1093                    let (rect, resp) = ui.allocate_exact_size(
1094                        ui.available_size(),
1095                        if matches!(
1096                            entry.departure.unwrap_or(TravelMode::Flexible),
1097                            TravelMode::Flexible
1098                        ) {
1099                            Sense::click()
1100                        } else {
1101                            Sense::click_and_drag()
1102                        },
1103                    );
1104                    ui.scope_builder(
1105                        UiBuilder::new()
1106                            .sense(resp.sense)
1107                            .max_rect(rect)
1108                            .id((entry_entity.inner().to_bits() as u128
1109                                | (line_index as u128) << 64)
1110                                ^ (1 << 127)),
1111                        |ui| {
1112                            ui.set_min_size(ui.available_size());
1113                            let response = ui.response();
1114                            let fill = if response.hovered() {
1115                                Color32::GRAY
1116                            } else {
1117                                Color32::WHITE
1118                            }
1119                            .linear_multiply(button_strength);
1120                            let handle_stroke = Stroke {
1121                                width: 2.5,
1122                                color: stroke.color.linear_multiply(button_strength),
1123                            };
1124                            match entry.departure {
1125                                Some(TravelMode::At(_)) => buttons::circle_button_shape(
1126                                    painter,
1127                                    departure_pos,
1128                                    CIRCLE_HANDLE_SIZE,
1129                                    handle_stroke,
1130                                    fill,
1131                                ),
1132                                Some(TravelMode::For(_)) => buttons::dash_button_shape(
1133                                    painter,
1134                                    departure_pos,
1135                                    DASH_HANDLE_SIZE,
1136                                    handle_stroke,
1137                                    fill,
1138                                ),
1139                                Some(TravelMode::Flexible) => buttons::triangle_button_shape(
1140                                    painter,
1141                                    departure_pos,
1142                                    TRIANGLE_HANDLE_SIZE,
1143                                    handle_stroke,
1144                                    fill,
1145                                ),
1146                                None => buttons::double_triangle(
1147                                    painter,
1148                                    departure_pos,
1149                                    DASH_HANDLE_SIZE,
1150                                    handle_stroke,
1151                                    fill,
1152                                ),
1153                            };
1154                        },
1155                    )
1156                    .response
1157                });
1158
1159            if departure_point_response.drag_started() {
1160                state.previous_total_drag_delta = None;
1161            }
1162            if let Some(total_drag_delta) = departure_point_response.total_drag_delta() {
1163                let previous_drag_delta = state.previous_total_drag_delta.unwrap_or(0.0);
1164                let duration = Duration(
1165                    ((total_drag_delta.x as f64 - previous_drag_delta as f64)
1166                        / state.zoom.x as f64
1167                        / TICKS_PER_SECOND as f64) as i32,
1168                );
1169                if duration != Duration(0) {
1170                    timetable_adjustment_writer.write(AdjustTimetableEntry {
1171                        entity: entry_entity.inner(),
1172                        adjustment: crate::vehicles::TimetableAdjustment::AdjustDepartureTime(
1173                            duration,
1174                        ),
1175                    });
1176                    state.previous_total_drag_delta = Some(
1177                        previous_drag_delta
1178                            + (duration.0 as f64 * TICKS_PER_SECOND as f64 * state.zoom.x as f64)
1179                                as f32,
1180                    );
1181                }
1182            }
1183            if departure_point_response.drag_stopped() {
1184                state.previous_total_drag_delta = None;
1185            }
1186            if departure_point_response.dragged() {
1187                departure_point_response.show_tooltip_ui(|ui| {
1188                    ui.label(entry_cache.estimate.as_ref().unwrap().departure.to_string());
1189                    ui.label(
1190                        station_names
1191                            .get(entry.station.entity())
1192                            .map_or("??", |s| s.as_str()),
1193                    );
1194                });
1195            } else {
1196                Popup::menu(&departure_point_response)
1197                    .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
1198                    .show(|ui| {
1199                        timetable_popup::popup(
1200                            entry_entity.inner(),
1201                            (entry, entry_cache),
1202                            previous_entry.and_then(|e| timetable_entries.get(e.inner()).ok()),
1203                            timetable_adjustment_writer,
1204                            ui,
1205                            false,
1206                        );
1207                    });
1208                previous_entry = Some(entry_entity);
1209            }
1210        }
1211    }
1212}
1213
1214fn draw_station_selection_overlay(
1215    _ui: &mut Ui,
1216    strength: f32,
1217    painter: &mut Painter,
1218    screen_rect: Rect,
1219    vertical_offset: f32,
1220    zoom_y: f32,
1221    station_entity: Instance<Station>,
1222    visible_stations: &[(Instance<Station>, f32)],
1223) {
1224    let stations = visible_stations
1225        .iter()
1226        .copied()
1227        .filter_map(|(s, h)| if s == station_entity { Some(h) } else { None });
1228    for station in stations {
1229        let station_height = (station - vertical_offset) * zoom_y + screen_rect.top();
1230        painter.rect(
1231            Rect::from_two_pos(
1232                Pos2 {
1233                    x: screen_rect.left(),
1234                    y: station_height,
1235                },
1236                Pos2 {
1237                    x: screen_rect.right(),
1238                    y: station_height,
1239                },
1240            )
1241            .expand2(Vec2 { x: -1.0, y: 7.0 }),
1242            4,
1243            Color32::BLUE.linear_multiply(strength * 0.5),
1244            Stroke::new(1.0, Color32::BLUE.linear_multiply(strength)),
1245            egui::StrokeKind::Middle,
1246        );
1247    }
1248}
1249
1250fn draw_interval_selection_overlay(
1251    _ui: &mut Ui,
1252    strength: f32,
1253    painter: &mut Painter,
1254    screen_rect: Rect,
1255    vertical_offset: f32,
1256    zoom_y: f32,
1257    (s1, s2): (Instance<Station>, Instance<Station>),
1258    visible_stations: &[(Instance<Station>, f32)],
1259) {
1260    for w in visible_stations.windows(2) {
1261        let [(e1, h1), (e2, h2)] = w else { continue };
1262        if !((*e1 == s1 && *e2 == s2) || (*e1 == s2 && *e2 == s1)) {
1263            continue;
1264        }
1265        let station_height_1 = (h1 - vertical_offset) * zoom_y + screen_rect.top();
1266        let station_height_2 = (h2 - vertical_offset) * zoom_y + screen_rect.top();
1267        painter.rect(
1268            Rect::from_two_pos(
1269                Pos2 {
1270                    x: screen_rect.left(),
1271                    y: station_height_1,
1272                },
1273                Pos2 {
1274                    x: screen_rect.right(),
1275                    y: station_height_2,
1276                },
1277            )
1278            .expand2(Vec2 { x: -1.0, y: 7.0 }),
1279            4,
1280            Color32::GREEN.linear_multiply(strength * 0.5),
1281            Stroke::new(1.0, Color32::GREEN.linear_multiply(strength)),
1282            egui::StrokeKind::Middle,
1283        );
1284    }
1285}
1286
1287fn draw_station_lines<'a, F>(
1288    vertical_offset: f32,
1289    stroke: Stroke,
1290    painter: &mut Painter,
1291    screen_rect: &Rect,
1292    zoom: f32,
1293    to_draw: &[(Instance<Station>, f32)],
1294    pixels_per_point: f32,
1295    text_color: Color32,
1296    mut get_station_name: F,
1297) where
1298    F: FnMut(Entity) -> Option<&'a str>,
1299{
1300    // Guard against invalid zoom
1301    for (entity, height) in to_draw.iter().copied() {
1302        let mut draw_height = (height - vertical_offset) * zoom + screen_rect.top();
1303        stroke.round_center_to_pixel(pixels_per_point, &mut draw_height);
1304        painter.hline(
1305            screen_rect.left()..=screen_rect.right(),
1306            draw_height,
1307            stroke,
1308        );
1309        let Some(station_name) = get_station_name(*entity) else {
1310            continue;
1311        };
1312        let layout = painter.layout_no_wrap(
1313            station_name.to_string(),
1314            egui::FontId::proportional(13.0),
1315            text_color,
1316        );
1317        let layout_pos = Pos2 {
1318            x: screen_rect.left(),
1319            y: draw_height - layout.size().y,
1320        };
1321        painter.galley(layout_pos, layout, text_color);
1322    }
1323}
1324
1325fn ticks_to_screen_x(
1326    ticks: i64,
1327    screen_rect: &Rect,
1328    ticks_per_screen_unit: f64,
1329    offset_ticks: i64,
1330) -> f32 {
1331    let base = (ticks - offset_ticks) as f64 / ticks_per_screen_unit;
1332    screen_rect.left() + base as f32
1333}
1334
1335/// Draw vertical time lines and labels
1336fn draw_time_lines(
1337    tick_offset: i64,
1338    stroke: Stroke,
1339    painter: &mut Painter,
1340    screen_rect: &Rect,
1341    ticks_per_screen_unit: f64,
1342    visible_ticks: &std::ops::Range<i64>,
1343    pixels_per_point: f32,
1344) {
1345    const MAX_SCREEN_WIDTH: f64 = 64.0;
1346    const MIN_SCREEN_WIDTH: f64 = 32.0;
1347    const SIZES: &[i64] = &[
1348        TICKS_PER_SECOND * 1,            // 1 second
1349        TICKS_PER_SECOND * 10,           // 10 seconds
1350        TICKS_PER_SECOND * 30,           // 30 seconds
1351        TICKS_PER_SECOND * 60,           // 1 minute
1352        TICKS_PER_SECOND * 60 * 5,       // 5 minutes
1353        TICKS_PER_SECOND * 60 * 10,      // 10 minutes
1354        TICKS_PER_SECOND * 60 * 30,      // 30 minutes
1355        TICKS_PER_SECOND * 60 * 60,      // 1 hour
1356        TICKS_PER_SECOND * 60 * 60 * 4,  // 4 hours
1357        TICKS_PER_SECOND * 60 * 60 * 24, // 1 day
1358    ];
1359    let mut drawn: Vec<i64> = Vec::with_capacity(30);
1360
1361    // align the first tick to a spacing boundary that is <= visible start.
1362    let first_visible_position = SIZES
1363        .iter()
1364        .position(|s| *s as f64 / ticks_per_screen_unit * 1.5 > MIN_SCREEN_WIDTH)
1365        .unwrap_or(0);
1366    let visible = &SIZES[first_visible_position..];
1367    for (i, spacing) in visible.iter().enumerate().rev() {
1368        let first = visible_ticks.start - visible_ticks.start.rem_euclid(*spacing) - spacing;
1369        let mut tick = first;
1370        let strength = (((*spacing as f64 / ticks_per_screen_unit * 1.5) - MIN_SCREEN_WIDTH)
1371            / (MAX_SCREEN_WIDTH - MIN_SCREEN_WIDTH))
1372            .clamp(0.0, 1.0);
1373        if strength < 0.1 {
1374            continue;
1375        }
1376        let mut current_stroke = stroke;
1377        if strength.is_finite() {
1378            // strange bug here
1379            current_stroke.color = current_stroke.color.gamma_multiply(strength as f32);
1380        }
1381        current_stroke.width = 0.5;
1382        while tick <= visible_ticks.end {
1383            tick += *spacing;
1384            if drawn.contains(&tick) {
1385                continue;
1386            }
1387            let mut x = ticks_to_screen_x(tick, screen_rect, ticks_per_screen_unit, tick_offset);
1388            current_stroke.round_center_to_pixel(pixels_per_point, &mut x);
1389            painter.vline(x, screen_rect.top()..=screen_rect.bottom(), current_stroke);
1390            drawn.push(tick);
1391            let time = TimetableTime((tick / 100) as i32);
1392            let text = match i + first_visible_position {
1393                0..=2 => time.to_hmsd().2.to_string(),
1394                3..=8 => format!("{}:{:02}", time.to_hmsd().0, time.to_hmsd().1),
1395                _ => time.to_string(),
1396            };
1397            let label = painter.layout_no_wrap(text, FontId::monospace(13.0), current_stroke.color);
1398            painter.galley(
1399                Pos2 {
1400                    x: x - label.size().x / 2.0,
1401                    y: screen_rect.top(),
1402                },
1403                label,
1404                current_stroke.color,
1405            );
1406        }
1407    }
1408}