mirror of https://codeberg.org/topola/topola.git
222 lines
8.6 KiB
Rust
222 lines
8.6 KiB
Rust
// SPDX-FileCopyrightText: 2024 Topola contributors
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
use geo::point;
|
|
use rstar::Envelope;
|
|
use topola::interactor::activity::{InteractiveEvent, InteractiveEventKind, InteractiveInput};
|
|
|
|
use crate::{
|
|
config::Config, displayer::Displayer, error_dialog::ErrorDialog, menu_bar::MenuBar,
|
|
painter::Painter, translator::Translator, workspace::Workspace,
|
|
};
|
|
|
|
pub struct Viewport {
|
|
pub transform: egui::emath::TSTransform,
|
|
/// how much should a single arrow key press scroll
|
|
pub kbd_scroll_delta_factor: f32,
|
|
pub scheduled_zoom_to_fit: bool,
|
|
}
|
|
|
|
impl Viewport {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
transform: egui::emath::TSTransform::new([0.0, 0.0].into(), 0.01),
|
|
kbd_scroll_delta_factor: 5.0,
|
|
scheduled_zoom_to_fit: false,
|
|
}
|
|
}
|
|
|
|
pub fn update(
|
|
&mut self,
|
|
config: &Config,
|
|
ctx: &egui::Context,
|
|
tr: &Translator,
|
|
menu_bar: &MenuBar,
|
|
error_dialog: &mut ErrorDialog,
|
|
maybe_workspace: Option<&mut Workspace>,
|
|
) -> egui::Rect {
|
|
egui::CentralPanel::default()
|
|
.show(ctx, |ui| {
|
|
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
|
// TODO: only request re-render if anything changed
|
|
ui.ctx().request_repaint();
|
|
let (id, viewport_rect) = ui.allocate_space(ui.available_size());
|
|
|
|
let (response, latest_pos) =
|
|
self.read_egui_response_and_latest_pos(id, viewport_rect, ctx, ui);
|
|
self.update_transform_by_input(ctx, latest_pos);
|
|
|
|
if let Some(workspace) = maybe_workspace {
|
|
let latest_point = point! {x: latest_pos.x as f64, y: -latest_pos.y as f64};
|
|
|
|
let interactive_input = InteractiveInput {
|
|
active_layer: workspace.appearance_panel.active_layer,
|
|
pointer_pos: latest_point,
|
|
dt: ctx.input(|i| i.stable_dt),
|
|
};
|
|
|
|
workspace.advance_state_by_dt(
|
|
tr,
|
|
error_dialog,
|
|
menu_bar.frame_timestep,
|
|
&interactive_input,
|
|
);
|
|
|
|
let mut painter = Painter::new(ui, self.transform, menu_bar.show_bboxes);
|
|
|
|
let interactive_event_kind =
|
|
if response.clicked_by(egui::PointerButton::Primary) {
|
|
Some(InteractiveEventKind::PointerPrimaryButtonClicked)
|
|
} else if response.drag_started_by(egui::PointerButton::Primary) {
|
|
Some(InteractiveEventKind::PointerPrimaryButtonDragStarted)
|
|
} else if response.drag_stopped_by(egui::PointerButton::Primary) {
|
|
Some(InteractiveEventKind::PointerPrimaryButtonDragStopped)
|
|
} else if response.clicked_by(egui::PointerButton::Secondary) {
|
|
Some(InteractiveEventKind::PointerSecondaryButtonClicked)
|
|
} else {
|
|
None
|
|
};
|
|
if let Some(kind) = interactive_event_kind {
|
|
let (ctrl, shift) = response
|
|
.ctx
|
|
.input(|i| (i.modifiers.ctrl, i.modifiers.shift));
|
|
let _ = workspace.update_state_for_event(
|
|
tr,
|
|
error_dialog,
|
|
menu_bar,
|
|
&interactive_input,
|
|
InteractiveEvent { kind, ctrl, shift },
|
|
);
|
|
} else if let Some((_, bsk, cur_bbox)) =
|
|
workspace.overlay.get_bbox_reselect(latest_point)
|
|
{
|
|
use topola::autorouter::selection::BboxSelectionKind;
|
|
painter.paint_bbox_with_color(
|
|
cur_bbox,
|
|
match bsk {
|
|
BboxSelectionKind::CompletelyInside => egui::Color32::YELLOW,
|
|
BboxSelectionKind::MerelyIntersects => egui::Color32::BLUE,
|
|
},
|
|
);
|
|
}
|
|
|
|
let mut displayer = Displayer::new(config, painter, workspace);
|
|
displayer.update(ctx, menu_bar);
|
|
|
|
self.zoom_to_fit_if_scheduled(&workspace, &viewport_rect);
|
|
}
|
|
|
|
viewport_rect
|
|
})
|
|
})
|
|
.inner
|
|
.inner
|
|
}
|
|
|
|
fn read_egui_response_and_latest_pos(
|
|
&self,
|
|
id: egui::Id,
|
|
viewport_rect: egui::Rect,
|
|
ctx: &egui::Context,
|
|
ui: &mut egui::Ui,
|
|
) -> (egui::Response, egui::Pos2) {
|
|
let response = ui.interact(viewport_rect, id, egui::Sense::click_and_drag());
|
|
|
|
// NOTE: we use `interact_pos` instead of `latest_pos` to handle "pointer gone"
|
|
// events more graceful
|
|
let latest_pos = self.transform.inverse()
|
|
* (response
|
|
.interact_pointer_pos()
|
|
.unwrap_or_else(|| ctx.input(|i| i.pointer.interact_pos().unwrap_or_default())));
|
|
|
|
// disable built-in behavior of arrow keys
|
|
if response.has_focus() {
|
|
response.ctx.memory_mut(|m| {
|
|
// we are only allowed to modify the focus lock filter if we have focus
|
|
m.set_focus_lock_filter(
|
|
id,
|
|
egui::EventFilter {
|
|
horizontal_arrows: true,
|
|
vertical_arrows: true,
|
|
..Default::default()
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
(response, latest_pos)
|
|
}
|
|
|
|
fn update_transform_by_input(&mut self, ctx: &egui::Context, latest_pos: egui::Pos2) {
|
|
let old_scaling = self.transform.scaling;
|
|
self.transform.scaling *= ctx.input(|i| i.zoom_delta());
|
|
|
|
self.transform.translation += latest_pos.to_vec2() * (old_scaling - self.transform.scaling);
|
|
self.transform.translation += ctx.input_mut(|i| {
|
|
// handle scrolling
|
|
let mut scroll_delta = core::mem::take(&mut i.smooth_scroll_delta);
|
|
|
|
// arrow keys
|
|
let kbd_sdf = self.kbd_scroll_delta_factor;
|
|
let mut pressed =
|
|
|key| i.consume_shortcut(&egui::KeyboardShortcut::new(egui::Modifiers::SHIFT, key));
|
|
use egui::Key;
|
|
scroll_delta.y += if pressed(Key::ArrowDown) {
|
|
kbd_sdf
|
|
} else if pressed(Key::ArrowUp) {
|
|
-kbd_sdf
|
|
} else {
|
|
0.0
|
|
};
|
|
scroll_delta.x += if pressed(Key::ArrowRight) {
|
|
kbd_sdf
|
|
} else if pressed(Key::ArrowLeft) {
|
|
-kbd_sdf
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
scroll_delta
|
|
});
|
|
}
|
|
|
|
fn zoom_to_fit_if_scheduled(&mut self, workspace: &Workspace, viewport_rect: &egui::Rect) {
|
|
if self.scheduled_zoom_to_fit {
|
|
let root_bbox = workspace
|
|
.interactor
|
|
.invoker()
|
|
.autorouter()
|
|
.board()
|
|
.layout()
|
|
.drawing()
|
|
.rtree()
|
|
.root()
|
|
.envelope();
|
|
|
|
let root_bbox_width = root_bbox.upper()[0] - root_bbox.lower()[0];
|
|
let root_bbox_height = root_bbox.upper()[1] - root_bbox.lower()[1];
|
|
|
|
self.transform.scaling = 0.8
|
|
* if root_bbox_width / root_bbox_height
|
|
>= (viewport_rect.width() as f64) / (viewport_rect.height() as f64)
|
|
{
|
|
viewport_rect.width() / root_bbox_width as f32
|
|
} else {
|
|
viewport_rect.height() / root_bbox_height as f32
|
|
};
|
|
|
|
self.transform.translation =
|
|
egui::Vec2::new(viewport_rect.center()[0], viewport_rect.center()[1])
|
|
- (self.transform.scaling
|
|
* egui::Pos2::new(
|
|
root_bbox.center()[0] as f32,
|
|
-root_bbox.center()[1] as f32,
|
|
))
|
|
.to_vec2();
|
|
}
|
|
|
|
self.scheduled_zoom_to_fit = false;
|
|
}
|
|
}
|