Timeline marker, undo and redo compute off the window thread (#1675)
Thanks @phanoob.
This commit is contained in:
1 parent
9b2a40781c
commit
d5640b5efb
7 files changed
+285
-11
No files matched your search
@@ -156,6 +156,16 @@ pub struct Snapshot {
|
|||||||
pub pending_moves: std::collections::BTreeMap<u64, solidcraft_doc::Mat>,
|
pub pending_moves: std::collections::BTreeMap<u64, solidcraft_doc::Mat>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A copy of a session that evaluates commands ahead of it ([`Session::ahead`]).
|
||||||
|
pub struct Ahead(Session);
|
||||||
|
|
||||||
|
impl Ahead {
|
||||||
|
/// Run `commands` (their results stay in the shared evaluation cache).
|
||||||
|
pub fn evaluate(self, commands: &[(String, Value)]) -> Result<()> {
|
||||||
|
self.0.preview_in_place(commands).map(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// What some commands would make, evaluated on a scratch copy of a session
|
/// What some commands would make, evaluated on a scratch copy of a session
|
||||||
/// ([`Session::preview`]). The session itself is not changed.
|
/// ([`Session::preview`]). The session itself is not changed.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -887,6 +897,17 @@ impl Session {
|
|||||||
self.scratch().preview_in_place(commands).map(|(after, replies)| Preview { before: visible(self), after: visible(&after), replies })
|
self.scratch().preview_in_place(commands).map(|(after, replies)| Preview { before: visible(self), after: visible(&after), replies })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run `commands` on a [`Session::scratch`] copy that keeps the undo and redo stacks: what
|
||||||
|
/// they evaluate lands in the evaluation cache the copy shares with this session, so running
|
||||||
|
/// them here afterwards finds it there. For a front end that computes a command which may
|
||||||
|
/// take a while (undo, the history marker moved) off its window thread first.
|
||||||
|
pub fn ahead(&self) -> Ahead {
|
||||||
|
let mut copy = self.scratch();
|
||||||
|
copy.undo.clone_from(&self.undo);
|
||||||
|
copy.redo.clone_from(&self.redo);
|
||||||
|
Ahead(copy)
|
||||||
|
}
|
||||||
|
|
||||||
fn preview_in_place(mut self, commands: &[(String, Value)]) -> Result<(Self, Vec<Value>)> {
|
fn preview_in_place(mut self, commands: &[(String, Value)]) -> Result<(Self, Vec<Value>)> {
|
||||||
if commands.is_empty() {
|
if commands.is_empty() {
|
||||||
return Err(EngineError::Other("nothing to preview".into()));
|
return Err(EngineError::Other("nothing to preview".into()));
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
//! Commands that recompute the history, computed off the window thread as OK is (#1201): the
|
||||||
|
//! history marker dragged or clicked, a feature suppressed, reordered or deleted from the
|
||||||
|
//! timeline, undo and redo, the marker put back after a sketch. On a part with a slow feature
|
||||||
|
//! after the marker (a line of text cut into a block), each of these ran that feature's boolean
|
||||||
|
//! on the window thread, and the window froze until it was done.
|
||||||
|
//!
|
||||||
|
//! The command is evaluated on a copy of the session ([`solidcraft_engine::Session::ahead`]),
|
||||||
|
//! which leaves what it computes in the evaluation cache the session shares. Done within the
|
||||||
|
//! app's wait ([`crate::dialogs::APPLY_AT_ONCE`]), the command just runs; otherwise the window
|
||||||
|
//! goes on, saying it computes, and the command runs for real once the evaluation is in, finding
|
||||||
|
//! its features in the cache. Commands asked for meanwhile wait their turn; a marker moved again
|
||||||
|
//! replaces the move still waiting, so a drag computes where it ends rather than every slot it
|
||||||
|
//! crossed.
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::SolveApp;
|
||||||
|
|
||||||
|
/// A command computing off the window thread.
|
||||||
|
pub struct Deferred {
|
||||||
|
done: std::sync::mpsc::Receiver<()>,
|
||||||
|
/// When it started (ms).
|
||||||
|
pub(crate) started: f64,
|
||||||
|
command: (String, Value),
|
||||||
|
/// The design's revision it was computed for: a design changed meanwhile computes it again.
|
||||||
|
revision: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deferred {
|
||||||
|
fn finished(&self) -> bool {
|
||||||
|
!matches!(self.done.try_recv(), Err(std::sync::mpsc::TryRecvError::Empty))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commands a later one of the same kind replaces while they wait: where the marker ends is all
|
||||||
|
/// that counts.
|
||||||
|
fn replaced_by_a_later_one(id: &str) -> bool {
|
||||||
|
id == "timeline.roll_to"
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SolveApp {
|
||||||
|
/// Run a command that may recompute much of the history, off the window thread when it takes
|
||||||
|
/// a while (see the module notes). `Ok(Null)` when it is left computing or waiting its turn:
|
||||||
|
/// its outcome then shows as any command's does (an error in the status line).
|
||||||
|
pub fn run_heavy(&mut self, id: &str, params: Value) -> Result<Value, String> {
|
||||||
|
if self.deferred.is_some() {
|
||||||
|
if replaced_by_a_later_one(id) && self.deferred_queue.last().is_some_and(|(q, _)| q == id) {
|
||||||
|
self.deferred_queue.pop();
|
||||||
|
}
|
||||||
|
self.deferred_queue.push((id.to_string(), params));
|
||||||
|
return Ok(Value::Null);
|
||||||
|
}
|
||||||
|
match self.begin_deferred(id, ¶ms) {
|
||||||
|
Some(job) => {
|
||||||
|
self.deferred = Some(job);
|
||||||
|
Ok(Value::Null)
|
||||||
|
}
|
||||||
|
None => self.run(id, params),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Since when a command has been computing off the window thread (ms), if one is.
|
||||||
|
pub fn computing_since(&self) -> Option<f64> {
|
||||||
|
self.deferred.as_ref().map(|d| d.started)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Each frame: a command whose evaluation is in runs, then the next one waiting starts.
|
||||||
|
pub(crate) fn poll_deferred(&mut self, ctx: &egui::Context) {
|
||||||
|
if let Some(job) = &self.deferred {
|
||||||
|
if !job.finished() {
|
||||||
|
ctx.request_repaint_after(std::time::Duration::from_millis(100));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(job) = self.deferred.take() {
|
||||||
|
let (id, params) = job.command;
|
||||||
|
let superseded = replaced_by_a_later_one(&id) && self.deferred_queue.first().is_some_and(|(q, _)| *q == id);
|
||||||
|
if job.revision != self.session.revision {
|
||||||
|
// The design changed meanwhile: computed again for the design as it is now.
|
||||||
|
let _ = self.run_heavy(&id, params);
|
||||||
|
} else if !superseded {
|
||||||
|
let _ = self.run(&id, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.deferred.is_none() && !self.deferred_queue.is_empty() {
|
||||||
|
let (id, params) = self.deferred_queue.remove(0);
|
||||||
|
let _ = self.run_heavy(&id, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start evaluating a command ahead. `None` when it should just run: not asked for
|
||||||
|
/// ([`SolveApp::apply_off_thread`]), a file operation in progress, no threads (the web), or
|
||||||
|
/// done within the wait.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn begin_deferred(&self, id: &str, params: &Value) -> Option<Deferred> {
|
||||||
|
let wait = self.apply_off_thread?;
|
||||||
|
if self.file_load.busy() || self.file_picker.is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ahead = self.session.ahead();
|
||||||
|
let command = (id.to_string(), params.clone());
|
||||||
|
let commands = vec![command.clone()];
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("history".into())
|
||||||
|
.spawn(move || {
|
||||||
|
let _ = ahead.evaluate(&commands);
|
||||||
|
let _ = tx.send(());
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
match rx.recv_timeout(wait) {
|
||||||
|
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||||
|
Some(Deferred { done: rx, started: crate::now_ms(), command, revision: self.session.revision })
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn begin_deferred(&self, _id: &str, _params: &Value) -> Option<Deferred> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
//! Commands that recompute the history off the window thread (`deferred`): the marker moved,
|
||||||
|
//! undo, each computed beside the window and run once in; a marker moved again while one move
|
||||||
|
//! computes replaces the move still waiting.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::scenario::Harness;
|
||||||
|
|
||||||
|
/// A 60 × 20 × 10 block, a 10 × 10 × 5 boss on it, and a 4 mm hole through the boss, the
|
||||||
|
/// marker at the end. Every command computes off the window thread (no wait at all).
|
||||||
|
fn block_boss_hole() -> Harness {
|
||||||
|
let mut h = Harness::new();
|
||||||
|
h.app.home.open = false;
|
||||||
|
for (id, p) in [
|
||||||
|
("solid.box", json!({"length": 60, "width": 20, "height": 10})),
|
||||||
|
("solid.box", json!({"corner": [5, 5, 10], "length": 10, "width": 10, "height": 5, "operation": "join"})),
|
||||||
|
("solid.cylinder", json!({"base": [10, 10, -1], "axis": [0, 0, 1], "radius": 2, "height": 20, "operation": "cut"})),
|
||||||
|
] {
|
||||||
|
h.app.run(id, p).unwrap();
|
||||||
|
}
|
||||||
|
h.app.apply_off_thread = Some(Duration::ZERO);
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frames until nothing computes or waits (a few seconds at most).
|
||||||
|
fn until_settled(h: &mut Harness) {
|
||||||
|
for _ in 0..500 {
|
||||||
|
if h.app.deferred.is_none() && h.app.deferred_queue.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
h.frames(1);
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
h.frames(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn marker(h: &Harness) -> Option<usize> {
|
||||||
|
h.app.session.doc.marker
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_marker_moved_computes_off_the_window_thread_then_moves() {
|
||||||
|
let mut h = block_boss_hole();
|
||||||
|
h.app.run_heavy("timeline.roll_to", json!({"position": 1})).unwrap();
|
||||||
|
assert!(h.app.computing_since().is_some(), "computing");
|
||||||
|
assert_eq!(marker(&h), None, "the design is as it was while it computes");
|
||||||
|
until_settled(&mut h);
|
||||||
|
assert_eq!(marker(&h), Some(1));
|
||||||
|
h.step(&json!({"expect": {"volume": 12000}})).unwrap();
|
||||||
|
// And back to the end, the boss and the hole recomputed (or found in the cache).
|
||||||
|
h.app.run_heavy("timeline.roll_to", json!({})).unwrap();
|
||||||
|
until_settled(&mut h);
|
||||||
|
assert_eq!(marker(&h), None);
|
||||||
|
let hole = std::f64::consts::PI * 4.0 * 15.0;
|
||||||
|
let v = h.app.run("inspect.measure", json!({})).unwrap()["total"]["volume_mm3"].as_f64().unwrap();
|
||||||
|
assert!((v - (12500.0 - hole)).abs() < 1.0, "{v}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_marker_moved_again_replaces_the_move_still_waiting() {
|
||||||
|
let mut h = block_boss_hole();
|
||||||
|
h.app.run_heavy("timeline.roll_to", json!({"position": 1})).unwrap();
|
||||||
|
assert!(h.app.computing_since().is_some());
|
||||||
|
// A drag: two more slots while the first computes. Only the last one waits.
|
||||||
|
h.app.run_heavy("timeline.roll_to", json!({"position": 2})).unwrap();
|
||||||
|
h.app.run_heavy("timeline.roll_to", json!({"position": 3})).unwrap();
|
||||||
|
assert_eq!(h.app.deferred_queue.len(), 1, "{:?}", h.app.deferred_queue);
|
||||||
|
until_settled(&mut h);
|
||||||
|
assert_eq!(marker(&h), None, "where the drag ended: the end of the history");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undo_computes_off_the_window_thread() {
|
||||||
|
let mut h = block_boss_hole();
|
||||||
|
let features = h.app.session.doc.features.len();
|
||||||
|
h.app.start("edit.undo");
|
||||||
|
assert!(h.app.computing_since().is_some(), "undo computing");
|
||||||
|
assert_eq!(h.app.session.doc.features.len(), features, "nothing undone yet");
|
||||||
|
until_settled(&mut h);
|
||||||
|
assert_eq!(h.app.session.doc.features.len(), features - 1, "the hole undone");
|
||||||
|
h.step(&json!({"expect": {"volume": 12500}})).unwrap();
|
||||||
|
h.app.start("edit.redo");
|
||||||
|
until_settled(&mut h);
|
||||||
|
assert_eq!(h.app.session.doc.features.len(), features, "and redone");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_design_changed_while_a_move_computes_is_computed_again() {
|
||||||
|
let mut h = block_boss_hole();
|
||||||
|
h.app.run_heavy("timeline.roll_to", json!({"position": 2})).unwrap();
|
||||||
|
assert!(h.app.computing_since().is_some());
|
||||||
|
// Something else changes the design meanwhile (a command that just runs).
|
||||||
|
h.app.run("solid.box", json!({"corner": [40, 5, 10], "length": 5, "width": 5, "height": 5, "operation": "join"})).unwrap();
|
||||||
|
let revision = h.app.session.revision;
|
||||||
|
until_settled(&mut h);
|
||||||
|
assert!(h.app.session.revision > revision, "the move ran after the change");
|
||||||
|
assert_eq!(marker(&h), Some(2));
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ mod colour_vision_tests;
|
|||||||
pub mod context_menu;
|
pub mod context_menu;
|
||||||
pub mod control;
|
pub mod control;
|
||||||
pub mod crash_ui;
|
pub mod crash_ui;
|
||||||
|
pub mod deferred;
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
pub mod design_properties;
|
pub mod design_properties;
|
||||||
pub mod dialogs;
|
pub mod dialogs;
|
||||||
@@ -97,6 +98,8 @@ mod expression_complete;
|
|||||||
mod extrude_edit_tests;
|
mod extrude_edit_tests;
|
||||||
mod file_load;
|
mod file_load;
|
||||||
mod file_picker;
|
mod file_picker;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod history_off_thread_tests;
|
||||||
pub use file_picker::FilePickerFuture;
|
pub use file_picker::FilePickerFuture;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod blend_arrow_tests;
|
mod blend_arrow_tests;
|
||||||
@@ -321,6 +324,10 @@ pub struct SolveApp {
|
|||||||
pub preview: preview::PreviewState,
|
pub preview: preview::PreviewState,
|
||||||
/// OK pressed on a feature that takes a while: its evaluation runs off the window thread.
|
/// OK pressed on a feature that takes a while: its evaluation runs off the window thread.
|
||||||
pub applying: Option<dialogs::Applying>,
|
pub applying: Option<dialogs::Applying>,
|
||||||
|
/// A command recomputing the history (the marker moved, undo) computing off the window
|
||||||
|
/// thread, and those asked for meanwhile ([`deferred`]).
|
||||||
|
pub deferred: Option<deferred::Deferred>,
|
||||||
|
pub deferred_queue: Vec<(String, Value)>,
|
||||||
/// How long OK waits for a feature before computing it off the window thread, saying so;
|
/// How long OK waits for a feature before computing it off the window thread, saying so;
|
||||||
/// `None`: it waits as long as it takes (headless use, tests). The desktop app sets it.
|
/// `None`: it waits as long as it takes (headless use, tests). The desktop app sets it.
|
||||||
pub apply_off_thread: Option<std::time::Duration>,
|
pub apply_off_thread: Option<std::time::Duration>,
|
||||||
@@ -435,6 +442,8 @@ impl SolveApp {
|
|||||||
slicer_menu: dialogs_save_mesh::SlicerMenu::default(),
|
slicer_menu: dialogs_save_mesh::SlicerMenu::default(),
|
||||||
preview: preview::PreviewState::default(),
|
preview: preview::PreviewState::default(),
|
||||||
applying: None,
|
applying: None,
|
||||||
|
deferred: None,
|
||||||
|
deferred_queue: Vec::new(),
|
||||||
apply_off_thread: None,
|
apply_off_thread: None,
|
||||||
last_command: None,
|
last_command: None,
|
||||||
esc_handled: false,
|
esc_handled: false,
|
||||||
@@ -843,6 +852,11 @@ impl SolveApp {
|
|||||||
self.dialog = Some(d);
|
self.dialog = Some(d);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Undo and redo can recompute much of the history: off the window thread when slow.
|
||||||
|
if matches!(id, "edit.undo" | "edit.redo") {
|
||||||
|
let _ = self.run_heavy(id, json!({}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
let _ = self.run(id, json!({}));
|
let _ = self.run(id, json!({}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -928,7 +942,9 @@ impl SolveApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn restore_marker(&mut self, marker: Option<usize>) {
|
fn restore_marker(&mut self, marker: Option<usize>) {
|
||||||
let _ = self.run("timeline.roll_to", marker.map(|m| json!({ "position": m })).unwrap_or_else(|| json!({})));
|
// Rolling forward past the features after the sketch recomputes them (an edited sketch
|
||||||
|
// under a text cut): off the window thread when it takes a while.
|
||||||
|
let _ = self.run_heavy("timeline.roll_to", marker.map(|m| json!({ "position": m })).unwrap_or_else(|| json!({})));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Everything shown as selected: the selection plus the open dialog's inputs.
|
/// Everything shown as selected: the selection plus the open dialog's inputs.
|
||||||
@@ -1320,6 +1336,7 @@ impl SolveApp {
|
|||||||
|
|
||||||
fn cad_ui(&mut self, ui: &mut egui::Ui, report_input: &mut Option<issue_report::CadInput>) {
|
fn cad_ui(&mut self, ui: &mut egui::Ui, report_input: &mut Option<issue_report::CadInput>) {
|
||||||
scenario::clear_handles();
|
scenario::clear_handles();
|
||||||
|
self.poll_deferred(ui.ctx());
|
||||||
toolbar::app_bar(self, ui);
|
toolbar::app_bar(self, ui);
|
||||||
issue_report::CadInput::block(self, ui, report_input);
|
issue_report::CadInput::block(self, ui, report_input);
|
||||||
file_load::show(self, ui.ctx());
|
file_load::show(self, ui.ctx());
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ pub(crate) fn delete_features(app: &mut SolveApp, ids: Vec<u64>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if with.is_empty() && fail.is_empty() {
|
if with.is_empty() && fail.is_empty() {
|
||||||
let _ = app.run("timeline.delete", json!({ "features": ids }));
|
let _ = app.run_heavy("timeline.delete", json!({ "features": ids }));
|
||||||
} else {
|
} else {
|
||||||
app.dialog = Some(crate::dialogs::Dialog::confirm_delete_many(ids, with.into_iter().collect(), fail.into_iter().collect()));
|
app.dialog = Some(crate::dialogs::Dialog::confirm_delete_many(ids, with.into_iter().collect(), fail.into_iter().collect()));
|
||||||
}
|
}
|
||||||
@@ -192,7 +192,7 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
Some(k) if k < n => json!({"position": k}),
|
Some(k) if k < n => json!({"position": k}),
|
||||||
_ => json!({}),
|
_ => json!({}),
|
||||||
};
|
};
|
||||||
let _ = app.run("timeline.roll_to", pos);
|
let _ = app.run_heavy("timeline.roll_to", pos);
|
||||||
}
|
}
|
||||||
x += 24.0;
|
x += 24.0;
|
||||||
}
|
}
|
||||||
@@ -341,11 +341,11 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
ui.close();
|
ui.close();
|
||||||
}
|
}
|
||||||
if ui.button(if *suppressed { "Unsuppress Features" } else { "Suppress Features" }).clicked() {
|
if ui.button(if *suppressed { "Unsuppress Features" } else { "Suppress Features" }).clicked() {
|
||||||
let _ = app.run("timeline.suppress", json!({"feature": id}));
|
let _ = app.run_heavy("timeline.suppress", json!({"feature": id}));
|
||||||
ui.close();
|
ui.close();
|
||||||
}
|
}
|
||||||
if ui.button(crate::i18n::tr_label("Roll History Marker Here")).clicked() {
|
if ui.button(crate::i18n::tr_label("Roll History Marker Here")).clicked() {
|
||||||
let _ = app.run("timeline.roll_to", json!({ "feature": id }));
|
let _ = app.run_heavy("timeline.roll_to", json!({ "feature": id }));
|
||||||
ui.close();
|
ui.close();
|
||||||
}
|
}
|
||||||
if ui.button(crate::i18n::tr_label("Find in Browser")).clicked() {
|
if ui.button(crate::i18n::tr_label("Find in Browser")).clicked() {
|
||||||
@@ -398,14 +398,14 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
let slot = slot_at(&xs, end, pp.x);
|
let slot = slot_at(&xs, end, pp.x);
|
||||||
match dg {
|
match dg {
|
||||||
Drag::Marker => {
|
Drag::Marker => {
|
||||||
let _ = app.run("timeline.roll_to", if slot >= n { json!({}) } else { json!({ "position": slot }) });
|
let _ = app.run_heavy("timeline.roll_to", if slot >= n { json!({}) } else { json!({ "position": slot }) });
|
||||||
}
|
}
|
||||||
Drag::Feature(id) => {
|
Drag::Feature(id) => {
|
||||||
let from = app.session.doc.feature_index(id).unwrap_or(0);
|
let from = app.session.doc.feature_index(id).unwrap_or(0);
|
||||||
// Dropping after itself means the slot shifts by one.
|
// Dropping after itself means the slot shifts by one.
|
||||||
let to = if slot > from { slot - 1 } else { slot };
|
let to = if slot > from { slot - 1 } else { slot };
|
||||||
if to != from {
|
if to != from {
|
||||||
let _ = app.run("timeline.reorder", json!({ "feature": id, "position": to }));
|
let _ = app.run_heavy("timeline.reorder", json!({ "feature": id, "position": to }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ fn timeline_transport(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
Some(position) if position < n => json!({"position": position}),
|
Some(position) if position < n => json!({"position": position}),
|
||||||
_ => json!({}),
|
_ => json!({}),
|
||||||
};
|
};
|
||||||
let _ = app.run("timeline.roll_to", params);
|
let _ = app.run_heavy("timeline.roll_to", params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -794,11 +794,11 @@ fn vertical_features(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
ui.close();
|
ui.close();
|
||||||
}
|
}
|
||||||
if ui.button(if *suppressed { "Unsuppress Feature" } else { "Suppress Feature" }).clicked() {
|
if ui.button(if *suppressed { "Unsuppress Feature" } else { "Suppress Feature" }).clicked() {
|
||||||
let _ = app.run("timeline.suppress", json!({"feature": id}));
|
let _ = app.run_heavy("timeline.suppress", json!({"feature": id}));
|
||||||
ui.close();
|
ui.close();
|
||||||
}
|
}
|
||||||
if ui.button("Roll Back Here").clicked() {
|
if ui.button("Roll Back Here").clicked() {
|
||||||
let _ = app.run("timeline.roll_to", json!({"feature": id}));
|
let _ = app.run_heavy("timeline.roll_to", json!({"feature": id}));
|
||||||
ui.close();
|
ui.close();
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
@@ -875,7 +875,7 @@ fn vertical_features(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
{
|
{
|
||||||
match active {
|
match active {
|
||||||
Drag::Marker => {
|
Drag::Marker => {
|
||||||
let _ = app.run("timeline.roll_to", if slot.position >= n { json!({}) } else { json!({"position": slot.position}) });
|
let _ = app.run_heavy("timeline.roll_to", if slot.position >= n { json!({}) } else { json!({"position": slot.position}) });
|
||||||
}
|
}
|
||||||
Drag::Items(items) => {
|
Drag::Items(items) => {
|
||||||
let panel_left = ui.min_rect().left();
|
let panel_left = ui.min_rect().left();
|
||||||
|
|||||||
@@ -2591,6 +2591,19 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
crate::select_other::show(app, ui.ctx());
|
crate::select_other::show(app, ui.ctx());
|
||||||
axis_view(app, ui, rect);
|
axis_view(app, ui, rect);
|
||||||
nav_bar(app, ui, rect);
|
nav_bar(app, ui, rect);
|
||||||
|
// A command recomputing the history off the window thread (the marker moved, undo): the
|
||||||
|
// window goes on, and says so above the status chip.
|
||||||
|
if let Some(since) = app.computing_since() {
|
||||||
|
let seconds = ((crate::now_ms() - since) / 1000.0).floor().max(0.0);
|
||||||
|
let text = crate::i18n::format("dialog.computing", &[("seconds", &format!("{seconds:.0}"))]);
|
||||||
|
let pos = pos2(rect.left() + 12.0, rect.bottom() - 76.0);
|
||||||
|
let galley = painter.layout_no_wrap(text, FontId::proportional(12.5), t.text);
|
||||||
|
let r = Rect::from_min_size(pos, galley.size() + vec2(34.0, 8.0));
|
||||||
|
painter.rect_filled(r, 4.0, t.overlay);
|
||||||
|
egui::Spinner::new().size(12.0).paint_at(ui, Rect::from_center_size(pos2(r.left() + 13.0, r.center().y), vec2(12.0, 12.0)));
|
||||||
|
painter.galley(pos + vec2(26.0, 4.0), galley, t.text);
|
||||||
|
ui.ctx().request_repaint_after(std::time::Duration::from_millis(100));
|
||||||
|
}
|
||||||
// Status chip.
|
// Status chip.
|
||||||
if let Some((msg, at, err)) = app.status.clone() {
|
if let Some((msg, at, err)) = app.status.clone() {
|
||||||
if crate::now_ms() - at < 6000.0 {
|
if crate::now_ms() - at < 6000.0 {
|
||||||
|
|||||||
Reference in new issue
Block a user