From c7430f9fc9b3cecb31fceccd04e88b88fc9e8cdd Mon Sep 17 00:00:00 2001 From: Mikolaj Wielgus Date: Thu, 4 Jun 2026 21:41:02 +0200 Subject: [PATCH] Add toggle button and slider to change step rate --- topola-egui/src/actions.rs | 24 ++++++++++++++++++++- topola-egui/src/app.rs | 7 +++++- topola-egui/src/menu_bar.rs | 35 ++++++++++++++++++++++++++++-- topola-egui/src/viewport.rs | 25 +++++++++++++++++++-- topola-egui/src/workspace.rs | 42 ++++++++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 6 deletions(-) diff --git a/topola-egui/src/actions.rs b/topola-egui/src/actions.rs index 57e3f17..702e054 100644 --- a/topola-egui/src/actions.rs +++ b/topola-egui/src/actions.rs @@ -2,19 +2,24 @@ // // SPDX-License-Identifier: MIT +use egui::{Context, Ui}; + use crate::{ - action::{Action, Trigger}, + action::{Action, Switch, Trigger}, + menu_bar::MenuBar, translator::Translator, }; pub struct Actions { pub file: FileActions, + pub debug: DebugActions, } impl Actions { pub fn new(tr: &Translator) -> Self { Self { 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); + } +} diff --git a/topola-egui/src/app.rs b/topola-egui/src/app.rs index e0201ca..7431584 100644 --- a/topola-egui/src/app.rs +++ b/topola-egui/src/app.rs @@ -134,7 +134,12 @@ impl eframe::App for App { 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_title(ctx); diff --git a/topola-egui/src/menu_bar.rs b/topola-egui/src/menu_bar.rs index 585e6ef..28d9c8f 100644 --- a/topola-egui/src/menu_bar.rs +++ b/topola-egui/src/menu_bar.rs @@ -4,6 +4,10 @@ use std::sync::mpsc::Sender; +use egui::{ + PopupCloseBehavior, + containers::menu::{MenuButton, MenuConfig}, +}; use serde::{Deserialize, Serialize}; use specctra::{ error::{ParseError, ParseErrorContext}, @@ -19,12 +23,16 @@ use crate::{ #[derive(Deserialize, Serialize)] pub struct MenuBar { - step_rate: f32, + pub fix_step_rate: bool, + pub step_rate: f64, } impl MenuBar { pub fn new() -> Self { - Self { step_rate: 1.0 } + Self { + fix_step_rate: false, + step_rate: 1.0, + } } pub fn update( @@ -42,6 +50,29 @@ impl MenuBar { }); 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); }); diff --git a/topola-egui/src/viewport.rs b/topola-egui/src/viewport.rs index d975ec8..a631ea3 100644 --- a/topola-egui/src/viewport.rs +++ b/topola-egui/src/viewport.rs @@ -5,7 +5,7 @@ use egui::Pos2; 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 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::Frame::canvas(ui.style()).show(ui, |ui| { ui.ctx().request_repaint(); @@ -51,6 +57,20 @@ impl Viewport { Self::fit_to_rect_in_scene(viewport_rect, scene_rect, zoom_range.into()); 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)); if escape_pressed { 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)); let primary_released = ctx.input(|i| i.pointer.button_released(egui::PointerButton::Primary)); + let delete_pressed = ctx.input(|i| i.key_pressed(egui::Key::Delete)); let mut maybe_pointer_on_scene: Option> = None; diff --git a/topola-egui/src/workspace.rs b/topola-egui/src/workspace.rs index 80601c8..86c97e8 100644 --- a/topola-egui/src/workspace.rs +++ b/topola-egui/src/workspace.rs @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: MIT OR Apache-2.0 +use std::{ops::ControlFlow, time::Instant}; + use topola::{Board, Workspace}; use crate::{layers_panel::LayersPanel, translator::Translator}; @@ -9,6 +11,7 @@ use crate::{layers_panel::LayersPanel, translator::Translator}; pub struct GuiWorkspace { pub workspace: Workspace, pub appearance_panel: LayersPanel, + pub dt_accum: f64, } impl GuiWorkspace { @@ -18,14 +21,53 @@ impl GuiWorkspace { Self { workspace: Workspace::new_board(board), appearance_panel, + dt_accum: 0.0, } } + pub fn advance_state_by_dt( + &mut self, + tr: &Translator, + step_rate: Option, + 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) { let Self { workspace, appearance_panel, + dt_accum, } = self; + appearance_panel.update(ctx, workspace.board()); } }