Feature history on the left side, with contiguous folders (#449)

The feature history can sit in a panel on the left, with folders that hold one unbroken run of features, drag and drop as one undo step, and a resizable divider. Timeline folders save under format 17.
This commit is contained in:
Alexander Nutz authored and GitHub committed 2026-10-13 13:01:31 -04:00
1 parent be55a49b04
commit a8b3b21fd0
17 files changed
+2336 -37

No files matched your search

+10 -2
View File
@@ -174,6 +174,7 @@ pub enum RenameWhat {
Feature(u64),
Component(u64),
Group(u64),
TimelineGroup(u64),
Canvas(u64),
View(String),
Joint(u64),
@@ -184,7 +185,7 @@ pub struct Rename {
pub what: RenameWhat,
pub text: String,
pub at: Pos2,
focused: bool,
pub(crate) focused: bool,
}
/// Radial slots, clockwise from the top.
@@ -1136,6 +1137,7 @@ pub fn start_rename(app: &mut SolveApp, what: RenameWhat, at: Pos2) {
RenameWhat::Component(0) => doc.name.clone(),
RenameWhat::Component(c) => doc.components.iter().find(|x| x.id == *c).map(|x| x.name.clone()).unwrap_or_default(),
RenameWhat::Group(g) => crate::browser::group_name(app, *g).unwrap_or_default(),
RenameWhat::TimelineGroup(g) => doc.timeline_groups.iter().find(|group| group.id == *g).map(|group| group.name.clone()).unwrap_or_default(),
RenameWhat::Canvas(c) => doc.canvases.iter().find(|x| x.id == *c).map(|x| x.name.clone()).unwrap_or_default(),
RenameWhat::View(v) => v.clone(),
RenameWhat::Joint(j) => doc.assembly.joints.iter().find(|x| x.id == *j).map(|x| x.name.clone()).unwrap_or_default(),
@@ -1143,7 +1145,7 @@ pub fn start_rename(app: &mut SolveApp, what: RenameWhat, at: Pos2) {
app.menu.rename = Some(Rename { what, text, at, focused: false });
}
fn commit_rename(app: &mut SolveApp, r: &Rename) {
pub(crate) fn commit_rename(app: &mut SolveApp, r: &Rename) {
let name = r.text.trim().to_string();
if name.is_empty() {
return;
@@ -1161,6 +1163,7 @@ fn commit_rename(app: &mut SolveApp, r: &Rename) {
RenameWhat::Feature(f) => drop(app.run("timeline.rename", json!({ "feature": f, "name": name }))),
RenameWhat::Component(c) => drop(app.run("component.rename", json!({ "component": c, "name": name }))),
RenameWhat::Group(g) => crate::browser::group_action(app, "ui.renameGroup", &json!({ "group": g, "name": name })),
RenameWhat::TimelineGroup(g) => drop(app.run("timeline.rename_group", json!({ "group": g, "name": name }))),
RenameWhat::Canvas(c) => drop(app.run("canvas.edit", json!({ "canvas": c, "name": name }))),
RenameWhat::View(v) => drop(app.run("view.rename", json!({ "view": v, "name": name }))),
RenameWhat::Joint(j) => drop(app.run("joint.edit", json!({ "joint": j, "name": name }))),
@@ -1566,6 +1569,11 @@ pub fn list_row_lit(ui: &mut egui::Ui, item: &Item, lit: bool) -> Option<Item> {
}
fn show_rename(app: &mut SolveApp, ctx: &egui::Context) {
// Feature-history folders own their editor inside the row. Keep the shared rename state so
// automation and keyboard handling still use the same path, but do not draw the floating box.
if app.preferences.timeline_left && matches!(app.menu.rename.as_ref().map(|rename| &rename.what), Some(RenameWhat::TimelineGroup(_))) {
return;
}
let Some(mut r) = app.menu.rename.take() else { return };
let t = Tokens::get();
let mut done: Option<bool> = None;