Add toggle button and slider to change step rate

This commit is contained in:
Mikolaj Wielgus 2026-06-04 21:41:02 +02:00
parent 0cd4b005fd
commit c7430f9fc9
5 changed files with 127 additions and 6 deletions

View File

@ -2,19 +2,24 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use egui::{Context, Ui};
use crate::{ use crate::{
action::{Action, Trigger}, action::{Action, Switch, Trigger},
menu_bar::MenuBar,
translator::Translator, translator::Translator,
}; };
pub struct Actions { pub struct Actions {
pub file: FileActions, pub file: FileActions,
pub debug: DebugActions,
} }
impl Actions { impl Actions {
pub fn new(tr: &Translator) -> Self { pub fn new(tr: &Translator) -> Self {
Self { Self {
file: FileActions::new(tr), file: FileActions::new(tr),
debug: DebugActions::new(tr),
} }
} }
} }
@ -68,3 +73,20 @@ impl FileActions {
} }
} }
} }
pub struct DebugActions {
pub fix_step_rate: Switch,
}
impl DebugActions {
pub fn new(tr: &Translator) -> Self {
Self {
fix_step_rate: Action::new_keyless(tr.text("tr-menu-debug-fix-step-rate"))
.into_switch(),
}
}
pub fn render_menu(&mut self, _ctx: &Context, ui: &mut Ui, menu_bar: &mut MenuBar) {
self.fix_step_rate.checkbox(ui, &mut menu_bar.fix_step_rate);
}
}

View File

@ -134,7 +134,12 @@ impl eframe::App for App {
workspace.update_appearance_panel(ctx); workspace.update_appearance_panel(ctx);
} }
self.viewport.update(ctx, self.workspace.as_mut()); self.viewport.update(
&self.translator,
ctx,
&self.menu_bar,
self.workspace.as_mut(),
);
self.update_locale(); self.update_locale();
self.update_title(ctx); self.update_title(ctx);

View File

@ -4,6 +4,10 @@
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use egui::{
PopupCloseBehavior,
containers::menu::{MenuButton, MenuConfig},
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specctra::{ use specctra::{
error::{ParseError, ParseErrorContext}, error::{ParseError, ParseErrorContext},
@ -19,12 +23,16 @@ use crate::{
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct MenuBar { pub struct MenuBar {
step_rate: f32, pub fix_step_rate: bool,
pub step_rate: f64,
} }
impl MenuBar { impl MenuBar {
pub fn new() -> Self { pub fn new() -> Self {
Self { step_rate: 1.0 } Self {
fix_step_rate: false,
step_rate: 1.0,
}
} }
pub fn update( pub fn update(
@ -42,6 +50,29 @@ impl MenuBar {
}); });
ui.separator(); ui.separator();
MenuButton::new(tr.text("tr-menu-debug"))
.config(
MenuConfig::default()
.close_behavior(PopupCloseBehavior::CloseOnClickOutside),
)
.ui(ui, |ui| {
actions.debug.render_menu(ctx, ui, self);
ui.add_enabled_ui(self.fix_step_rate, |ui| {
ui.label(tr.text("tr-menu-debug-step-rate"));
ui.add(
egui::widgets::Slider::new(&mut self.step_rate, 0.1..=1000.0)
.suffix(format!(
" {}",
tr.text("tr-menu-debug-step-rate-unit")
)),
);
});
});
ui.separator();
egui::widgets::global_theme_preference_switch(ui); egui::widgets::global_theme_preference_switch(ui);
}); });

View File

@ -5,7 +5,7 @@
use egui::Pos2; use egui::Pos2;
use topola::{MasterInteractor, Vector2, Workspace}; use topola::{MasterInteractor, Vector2, Workspace};
use crate::{display::Display, workspace::GuiWorkspace}; use crate::{display::Display, menu_bar::MenuBar, translator::Translator, workspace::GuiWorkspace};
pub struct Viewport { pub struct Viewport {
pub scene_rect: egui::Rect, pub scene_rect: egui::Rect,
@ -24,7 +24,13 @@ impl Viewport {
} }
} }
pub fn update(&mut self, ctx: &egui::Context, workspace: Option<&mut GuiWorkspace>) { pub fn update(
&mut self,
tr: &Translator,
ctx: &egui::Context,
menu_bar: &MenuBar,
workspace: Option<&mut GuiWorkspace>,
) {
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
egui::Frame::canvas(ui.style()).show(ui, |ui| { egui::Frame::canvas(ui.style()).show(ui, |ui| {
ui.ctx().request_repaint(); ui.ctx().request_repaint();
@ -51,6 +57,20 @@ impl Viewport {
Self::fit_to_rect_in_scene(viewport_rect, scene_rect, zoom_range.into()); Self::fit_to_rect_in_scene(viewport_rect, scene_rect, zoom_range.into());
if let Some(workspace) = workspace { if let Some(workspace) = workspace {
workspace.advance_state_by_dt(
tr,
menu_bar.fix_step_rate.then_some(menu_bar.step_rate),
ctx.input(|i| {
if i.stable_dt <= i.predicted_dt {
i.stable_dt
} else {
// Clamp dt to egui's predicted dt to
// additionally safeguard against stuttering.
i.predicted_dt
}
}) as f64,
);
let escape_pressed = ctx.input(|i| i.key_pressed(egui::Key::Escape)); let escape_pressed = ctx.input(|i| i.key_pressed(egui::Key::Escape));
if escape_pressed { if escape_pressed {
if let Some(interactor) = self.master_interactor.as_mut() { if let Some(interactor) = self.master_interactor.as_mut() {
@ -70,6 +90,7 @@ impl Viewport {
ctx.input(|i| i.pointer.button_down(egui::PointerButton::Primary)); ctx.input(|i| i.pointer.button_down(egui::PointerButton::Primary));
let primary_released = let primary_released =
ctx.input(|i| i.pointer.button_released(egui::PointerButton::Primary)); ctx.input(|i| i.pointer.button_released(egui::PointerButton::Primary));
let delete_pressed = ctx.input(|i| i.key_pressed(egui::Key::Delete)); let delete_pressed = ctx.input(|i| i.key_pressed(egui::Key::Delete));
let mut maybe_pointer_on_scene: Option<Vector2<i64>> = None; let mut maybe_pointer_on_scene: Option<Vector2<i64>> = None;

View File

@ -2,6 +2,8 @@
// //
// SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-License-Identifier: MIT OR Apache-2.0
use std::{ops::ControlFlow, time::Instant};
use topola::{Board, Workspace}; use topola::{Board, Workspace};
use crate::{layers_panel::LayersPanel, translator::Translator}; use crate::{layers_panel::LayersPanel, translator::Translator};
@ -9,6 +11,7 @@ use crate::{layers_panel::LayersPanel, translator::Translator};
pub struct GuiWorkspace { pub struct GuiWorkspace {
pub workspace: Workspace, pub workspace: Workspace,
pub appearance_panel: LayersPanel, pub appearance_panel: LayersPanel,
pub dt_accum: f64,
} }
impl GuiWorkspace { impl GuiWorkspace {
@ -18,14 +21,53 @@ impl GuiWorkspace {
Self { Self {
workspace: Workspace::new_board(board), workspace: Workspace::new_board(board),
appearance_panel, appearance_panel,
dt_accum: 0.0,
} }
} }
pub fn advance_state_by_dt(
&mut self,
tr: &Translator,
step_rate: Option<f64>,
dt: f64,
) -> bool {
let instant = Instant::now();
if step_rate.is_some() {
self.dt_accum += dt;
}
while step_rate.is_none_or(|step_rate| self.dt_accum >= 1.0 / step_rate) {
if let Some(step_rate) = step_rate {
self.dt_accum -= 1.0 / step_rate;
}
if let ControlFlow::Break(()) = self.step(tr) {
return true;
}
// Hard limit: never spend more time on advancing state than the
// duration of last frame to prevent stuttering.
// Of course, this does not safeguard against infinite loops.
if instant.elapsed().as_secs_f64() >= dt {
return false;
}
}
true
}
pub fn step(&mut self, tr: &Translator) -> ControlFlow<()> {
ControlFlow::Continue(())
}
pub fn update_appearance_panel(&mut self, ctx: &egui::Context) { pub fn update_appearance_panel(&mut self, ctx: &egui::Context) {
let Self { let Self {
workspace, workspace,
appearance_panel, appearance_panel,
dt_accum,
} = self; } = self;
appearance_panel.update(ctx, workspace.board()); appearance_panel.update(ctx, workspace.board());
} }
} }