M6: view a version on the canvas before restoring it (#36)
Clicking a version in Version history shows it in place of the document, read-only, with a banner to restore it or go back to the current version (clicking Current version or closing the panel goes back too). Edits, undo and gestures are refused while a version is shown; saving, closing and restoring end the preview first, so the document and its unsaved state are never touched by it. New commands version.preview and version.exitPreview; version.list marks the shown version. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
360b6df206
commit
ce317cdf42
|
|
@ -159,7 +159,7 @@ Scores by behaviour, 0–100%. Hours are what remains (one agent).
|
|||
| Prototyping | 6 | 70% | scrolling inside device frames, prototype preview pane in the editor | 3–5 |
|
||||
| Import/export | 6 | 50% | PDF (in progress), .fig import, export panel polish, REST import of prototypes/variables | 18–32 |
|
||||
| Dev mode / inspect | 2 | 55% | asset export list in Inspect, redlines in the Inspect tab, code for whole frames as components | 4–6 |
|
||||
| Comments, versions, misc | 3 | 35% | version preview on the canvas, autosave checkpoints, comment polish, plugins (WASM) | 12–20 |
|
||||
| Comments, versions, misc | 3 | 40% | autosave checkpoints, comment polish, plugins (WASM) | 10–17 |
|
||||
|
||||
Weighted feature depth ≈ **58%** (self-graded; the table's arithmetic gives ~59%).
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
spec!(
|
||||
"version.list",
|
||||
"List versions",
|
||||
"{} the version history, oldest first: [{id, index, name, description, time (Unix seconds)}]",
|
||||
"{} the version history, oldest first: [{id, index, name, description, time (Unix seconds), previewing?}]",
|
||||
false,
|
||||
|s, _| Ok(version_list(s.ds()?))
|
||||
),
|
||||
|
|
@ -125,9 +125,30 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
spec!("version.delete", "Delete version", "{id | index}", false, |s, p| {
|
||||
let ds = s.ds_mut()?;
|
||||
let i = version_index("version.delete", ds, p)?;
|
||||
ds.versions_mut().remove(i);
|
||||
let v = ds.versions_mut().remove(i);
|
||||
if ds.preview.as_ref().is_some_and(|pv| pv.id == v.id) {
|
||||
ds.end_preview();
|
||||
}
|
||||
ok()
|
||||
}),
|
||||
spec!(
|
||||
"version.preview",
|
||||
"View version",
|
||||
"{id | index} show the version on the canvas in place of the document (read-only; `version.restore` keeps it, `version.exitPreview` goes back)",
|
||||
false,
|
||||
|s, p| {
|
||||
let ds = s.ds_mut()?;
|
||||
let i = version_index("version.preview", ds, p)?;
|
||||
let v = ds.versions.get(i).ok_or_else(|| bad("version.preview", "no such version"))?;
|
||||
let (id, name, time, out) = (v.id.clone(), v.name.clone(), v.time, version_json(i, v));
|
||||
let doc = read_version(v)?;
|
||||
ds.start_preview(&id, &name, time, doc);
|
||||
Ok(out)
|
||||
}
|
||||
),
|
||||
spec!("version.exitPreview", "Back to current version", "{} end a version preview", false, |s, _| {
|
||||
Ok(json!({"ended": s.ds_mut()?.end_preview()}))
|
||||
}),
|
||||
spec!(
|
||||
"version.duplicate",
|
||||
"Duplicate version",
|
||||
|
|
@ -173,7 +194,20 @@ fn version_json(index: usize, v: &Version) -> Value {
|
|||
}
|
||||
|
||||
fn version_list(ds: &DocState) -> Value {
|
||||
Value::Array(ds.versions.iter().enumerate().map(|(i, v)| version_json(i, v)).collect())
|
||||
let previewing = ds.preview.as_ref().map(|p| p.id.as_str());
|
||||
Value::Array(
|
||||
ds.versions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
let mut j = version_json(i, v);
|
||||
if previewing == Some(v.id.as_str()) {
|
||||
j["previewing"] = json!(true);
|
||||
}
|
||||
j
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The version named by `id`, else by `index` (validated: never negative, never past the end).
|
||||
|
|
|
|||
|
|
@ -226,8 +226,29 @@ pub struct DocState {
|
|||
pub versions: Vec<figcraft_format::Version>,
|
||||
/// The version history changed since the last save.
|
||||
versions_changed: bool,
|
||||
/// A version shown in place of the document (read-only).
|
||||
pub preview: Option<VersionPreview>,
|
||||
}
|
||||
|
||||
/// A version shown on the canvas in place of the document: read-only until it's restored or the
|
||||
/// preview ends, which puts the document back as it was.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VersionPreview {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
/// When the version was saved (Unix seconds).
|
||||
pub time: u64,
|
||||
doc: Arc<Document>,
|
||||
selection: Vec<NodeId>,
|
||||
page: NodeId,
|
||||
}
|
||||
|
||||
/// Why edits are refused while a version is shown.
|
||||
pub const PREVIEW_READ_ONLY: &str = "you're viewing an earlier version: restore it or go back to the current version to edit";
|
||||
|
||||
/// Commands that first end a version preview: they act on the document itself.
|
||||
const ENDS_PREVIEW: &[&str] = &["file.save", "file.saveAs", "file.close", "file.revert", "file.saveVersion", "file.duplicate", "version.restore"];
|
||||
|
||||
static NEXT_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
|
||||
impl DocState {
|
||||
|
|
@ -253,6 +274,7 @@ impl DocState {
|
|||
uid: NEXT_UID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||
versions: Vec::new(),
|
||||
versions_changed: false,
|
||||
preview: None,
|
||||
};
|
||||
st.zoom_to_fit();
|
||||
st
|
||||
|
|
@ -271,7 +293,44 @@ impl DocState {
|
|||
}
|
||||
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
!Arc::ptr_eq(&self.doc, &self.saved_doc) || self.versions_changed
|
||||
let doc = self.preview.as_ref().map_or(&self.doc, |p| &p.doc);
|
||||
!Arc::ptr_eq(doc, &self.saved_doc) || self.versions_changed
|
||||
}
|
||||
|
||||
/// Show `version` in place of the document (or switch to it from another preview).
|
||||
pub fn start_preview(&mut self, id: &str, name: &str, time: u64, doc: Arc<Document>) {
|
||||
self.end_preview();
|
||||
self.preview = Some(VersionPreview {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
time,
|
||||
doc: std::mem::replace(&mut self.doc, doc),
|
||||
selection: std::mem::take(&mut self.selection),
|
||||
page: self.page,
|
||||
});
|
||||
self.page = Some(self.page)
|
||||
.filter(|pg| self.doc.pages.contains(pg))
|
||||
.or_else(|| self.doc.current_page.filter(|pg| self.doc.pages.contains(pg)))
|
||||
.or_else(|| self.doc.first_page())
|
||||
.unwrap_or(self.page);
|
||||
self.text_edit = None;
|
||||
self.vector_edit = None;
|
||||
self.interaction = None;
|
||||
self.revision += 1;
|
||||
}
|
||||
|
||||
/// End a version preview, putting the document, selection and page back. False when none.
|
||||
pub fn end_preview(&mut self) -> bool {
|
||||
let Some(p) = self.preview.take() else { return false };
|
||||
self.doc = p.doc;
|
||||
self.selection = p.selection;
|
||||
self.page = p.page;
|
||||
self.text_edit = None;
|
||||
self.vector_edit = None;
|
||||
self.interaction = None;
|
||||
self.clean_selection();
|
||||
self.revision += 1;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn mark_saved(&mut self) {
|
||||
|
|
@ -462,8 +521,15 @@ impl Session {
|
|||
/// as it was.
|
||||
pub fn execute(&mut self, id: &str, params: &Value) -> Result<Value> {
|
||||
let spec = find_command(id).ok_or_else(|| EngineError::UnknownCommand(id.to_string()))?;
|
||||
if ENDS_PREVIEW.contains(&id)
|
||||
&& let Ok(ds) = self.ds_mut()
|
||||
{
|
||||
ds.end_preview();
|
||||
}
|
||||
let before = self.ds().ok().map(|d| (d.doc.clone(), d.selection.clone(), d.page, d.uid));
|
||||
let result = guard::run(id, || (spec.run)(self, params)).and_then(|v| self.check_instance_structure(id, &before).map(|()| v));
|
||||
let result = guard::run(id, || (spec.run)(self, params))
|
||||
.and_then(|v| self.check_instance_structure(id, &before).map(|()| v))
|
||||
.and_then(|v| self.check_preview(id, &before).map(|()| v));
|
||||
match &result {
|
||||
Ok(_) => {
|
||||
self.after_command(spec, before);
|
||||
|
|
@ -490,6 +556,16 @@ impl Session {
|
|||
result
|
||||
}
|
||||
|
||||
/// A version shown in place of the document is read-only: refuse an edit (it is rolled back).
|
||||
fn check_preview(&self, id: &str, before: &Option<(Arc<Document>, Vec<NodeId>, NodeId, u64)>) -> Result<()> {
|
||||
let Some((bdoc, _, _, uid)) = before else { return Ok(()) };
|
||||
let Some(ds) = self.docs.iter().find(|d| d.uid == *uid) else { return Ok(()) };
|
||||
if ds.preview.is_none() || Arc::ptr_eq(&ds.doc, bdoc) || id == "version.preview" {
|
||||
return Ok(());
|
||||
}
|
||||
Err(EngineError::Disabled(id.to_string(), PREVIEW_READ_ONLY.into()))
|
||||
}
|
||||
|
||||
/// Instances can't gain, lose or reorder layers, and library copies are read-only: refuse such
|
||||
/// an edit (it is rolled back).
|
||||
fn check_instance_structure(&self, id: &str, before: &Option<(Arc<Document>, Vec<NodeId>, NodeId, u64)>) -> Result<()> {
|
||||
|
|
@ -535,6 +611,9 @@ impl Session {
|
|||
/// Start a gesture: later [`preview`](Self::preview) calls re-apply edits to this snapshot.
|
||||
pub fn begin_interaction(&mut self, label: &str) -> Result<()> {
|
||||
let ds = self.ds_mut()?;
|
||||
if ds.preview.is_some() {
|
||||
return Err(EngineError::Disabled(label.to_string(), PREVIEW_READ_ONLY.into()));
|
||||
}
|
||||
if ds.interaction.is_none() {
|
||||
ds.interaction = Some(Interaction { label: label.to_string(), doc: ds.doc.clone(), selection: ds.selection.clone() });
|
||||
}
|
||||
|
|
@ -585,6 +664,9 @@ impl Session {
|
|||
|
||||
pub fn undo(&mut self) -> Result<bool> {
|
||||
let ds = self.ds_mut()?;
|
||||
if ds.preview.is_some() {
|
||||
return Err(EngineError::Disabled("edit.undo".into(), PREVIEW_READ_ONLY.into()));
|
||||
}
|
||||
ds.interaction = None;
|
||||
ds.text_edit = None;
|
||||
let Some(e) = ds.history.undo.pop() else { return Ok(false) };
|
||||
|
|
@ -602,6 +684,9 @@ impl Session {
|
|||
|
||||
pub fn redo(&mut self) -> Result<bool> {
|
||||
let ds = self.ds_mut()?;
|
||||
if ds.preview.is_some() {
|
||||
return Err(EngineError::Disabled("edit.redo".into(), PREVIEW_READ_ONLY.into()));
|
||||
}
|
||||
ds.interaction = None;
|
||||
ds.text_edit = None;
|
||||
let Some(e) = ds.history.redo.pop() else { return Ok(false) };
|
||||
|
|
@ -787,4 +872,6 @@ mod variable_tests;
|
|||
#[cfg(test)]
|
||||
mod variables_editor_tests;
|
||||
#[cfg(test)]
|
||||
mod version_preview_tests;
|
||||
#[cfg(test)]
|
||||
mod version_tests;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
//! Viewing a version on the canvas: read-only, restore from it, back to the current version, and
|
||||
//! the document (and its unsaved state) untouched by the preview.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::params::id_of;
|
||||
use crate::tools::{PointerEvent, PointerKind};
|
||||
use crate::{EngineError, Session};
|
||||
|
||||
fn rect(s: &mut Session, x: f64) -> figcraft_doc::NodeId {
|
||||
id_of(&s.execute("shape.rectangle", &json!({"x": x, "y": 0, "width": 10, "height": 10})).unwrap()["id"]).unwrap()
|
||||
}
|
||||
|
||||
fn count(s: &Session) -> usize {
|
||||
s.doc().unwrap().node_count()
|
||||
}
|
||||
|
||||
/// A session with versions "One" (one box) and "Two" (two), then a third box and a selection.
|
||||
fn setup() -> (Session, figcraft_doc::NodeId) {
|
||||
let mut s = Session::new();
|
||||
rect(&mut s, 0.0);
|
||||
s.execute("file.saveVersion", &json!({"name": "One"})).unwrap();
|
||||
rect(&mut s, 20.0);
|
||||
s.execute("file.saveVersion", &json!({"name": "Two"})).unwrap();
|
||||
let r = rect(&mut s, 40.0);
|
||||
s.execute("select.set", &json!({"ids": [r]})).unwrap();
|
||||
(s, r)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_shows_the_version_read_only_and_goes_back() {
|
||||
let (mut s, r) = setup();
|
||||
let three = count(&s);
|
||||
let undo_depth = s.ds().unwrap().history.undo.len();
|
||||
let v = s.execute("version.preview", &json!({"index": 0})).unwrap();
|
||||
assert_eq!(v["name"], "One");
|
||||
assert_eq!(count(&s), three - 2);
|
||||
assert!(s.selection().is_empty());
|
||||
let l = s.execute("version.list", &json!({})).unwrap();
|
||||
assert_eq!(l[0]["previewing"], true);
|
||||
assert!(l[1].get("previewing").is_none());
|
||||
// Edits are refused and leave it as it was; so do undo and gestures.
|
||||
let err = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 5, "height": 5})).unwrap_err();
|
||||
assert!(matches!(err, EngineError::Disabled(..)), "{err:?}");
|
||||
assert_eq!(count(&s), three - 2);
|
||||
assert!(s.execute("edit.undo", &json!({})).is_err());
|
||||
assert_eq!(s.ds().unwrap().history.undo.len(), undo_depth, "history untouched");
|
||||
assert!(s.begin_interaction("Move").is_err());
|
||||
let _ = s.pointer(&PointerEvent::at(PointerKind::Down, 5.0, 5.0, 1.0));
|
||||
let _ = s.pointer(&PointerEvent::at(PointerKind::Move, 50.0, 50.0, 1.0));
|
||||
let _ = s.pointer(&PointerEvent::at(PointerKind::Up, 50.0, 50.0, 1.0));
|
||||
assert_eq!(count(&s), three - 2);
|
||||
// Switching to another version, then back to the current one.
|
||||
s.execute("version.preview", &json!({"index": 1})).unwrap();
|
||||
assert_eq!(count(&s), three - 1);
|
||||
assert_eq!(s.execute("version.exitPreview", &json!({})).unwrap()["ended"], true);
|
||||
assert_eq!(count(&s), three);
|
||||
assert_eq!(s.selection(), vec![r], "selection comes back");
|
||||
assert_eq!(s.execute("version.exitPreview", &json!({})).unwrap()["ended"], false);
|
||||
// Editing works again.
|
||||
rect(&mut s, 60.0);
|
||||
assert_eq!(count(&s), three + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_from_a_preview_is_one_undo_step() {
|
||||
let (mut s, _) = setup();
|
||||
let three = count(&s);
|
||||
s.execute("version.preview", &json!({"index": 0})).unwrap();
|
||||
s.execute("version.restore", &json!({"index": 0})).unwrap();
|
||||
assert!(s.ds().unwrap().preview.is_none());
|
||||
assert_eq!(count(&s), three - 2);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(count(&s), three, "undo goes back to the document, not the preview");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_keeps_the_unsaved_state_and_saving_saves_the_document() {
|
||||
let dir = std::env::temp_dir().join(format!("figcraft-vpreview-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("preview.figcraft").to_string_lossy().to_string();
|
||||
let (mut s, _) = setup();
|
||||
let three = count(&s);
|
||||
s.execute("file.save", &json!({"path": path})).unwrap();
|
||||
s.execute("version.preview", &json!({"index": 0})).unwrap();
|
||||
assert!(!s.ds().unwrap().is_dirty(), "looking at a version isn't a change");
|
||||
// Saving ends the preview and saves the document itself.
|
||||
s.execute("file.save", &json!({})).unwrap();
|
||||
assert!(s.ds().unwrap().preview.is_none());
|
||||
let mut t = Session::empty();
|
||||
t.execute("file.open", &json!({"path": path})).unwrap();
|
||||
assert_eq!(count(&t), three);
|
||||
// Deleting the version being viewed ends the preview.
|
||||
s.execute("version.preview", &json!({"index": 1})).unwrap();
|
||||
s.execute("version.delete", &json!({"index": 1})).unwrap();
|
||||
assert!(s.ds().unwrap().preview.is_none());
|
||||
assert_eq!(count(&s), three);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
|
@ -172,6 +172,7 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) {
|
|||
crate::handles::draw(app, &painter);
|
||||
crate::comments::draw(app, &painter);
|
||||
crate::comments::popovers(app, &ctx);
|
||||
crate::versions::preview_banner(app, &ctx, rect);
|
||||
// Cursor.
|
||||
if resp.hovered() {
|
||||
let c = if app.canvas.space_pan || app.canvas.middle_pan.is_some() {
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ pub enum Icon {
|
|||
Reset,
|
||||
Detach,
|
||||
Variable,
|
||||
Clock,
|
||||
Eraser,
|
||||
Swap,
|
||||
Link,
|
||||
|
|
@ -645,6 +646,12 @@ pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) {
|
|||
p.arc(8.0, 8.0, 5.0, -2.2, 2.6);
|
||||
p.poly(&[(2.8, 2.8), (3.3, 5.8), (6.3, 5.3)], false);
|
||||
}
|
||||
Clock => {
|
||||
// A dial with hands at ten past two-ish (version history).
|
||||
p.circle(8.0, 8.0, 6.0);
|
||||
p.line(8.0, 8.0, 8.0, 4.5);
|
||||
p.line(8.0, 8.0, 10.5, 9.5);
|
||||
}
|
||||
Eraser => {
|
||||
// A tilted block with a band near its tip, over a short trace line.
|
||||
p.poly(&[(3.0, 10.0), (9.5, 3.5), (13.0, 7.0), (6.5, 13.5)], true);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//! The Version history panel, shown in the left sidebar in place of Pages and Layers: save a
|
||||
//! version (title and description), the history newest first under the current version, and per
|
||||
//! version Restore, Rename, Duplicate and Delete.
|
||||
//! version Restore, Rename, Duplicate and Delete. Clicking a version shows it on the canvas
|
||||
//! (read-only, with a banner to restore it or go back); clicking Current version goes back.
|
||||
|
||||
use egui::{Align2, CornerRadius, Id, Rect, Sense, Stroke, Ui, pos2, vec2};
|
||||
use serde_json::json;
|
||||
|
|
@ -58,6 +59,8 @@ struct Row {
|
|||
|
||||
enum Action {
|
||||
Close,
|
||||
Preview(String),
|
||||
ExitPreview,
|
||||
New,
|
||||
Submit,
|
||||
CancelForm,
|
||||
|
|
@ -112,11 +115,14 @@ pub fn panel(app: &mut FigApp, ui: &mut Ui) {
|
|||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let previewing = app.session.ds().ok().and_then(|ds| ds.preview.as_ref().map(|p| p.id.clone()));
|
||||
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
|
||||
// Rows touch so the timeline is continuous.
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
ui.add_space(4.0);
|
||||
current_row(ui, !rows.is_empty());
|
||||
if current_row(ui, !rows.is_empty(), previewing.is_none()) {
|
||||
action = Some(Action::ExitPreview);
|
||||
}
|
||||
let last = rows.len().saturating_sub(1);
|
||||
for (i, row) in rows.iter().enumerate() {
|
||||
let a = if app.ui.versions.confirm_delete.as_deref() == Some(row.id.as_str()) {
|
||||
|
|
@ -124,7 +130,7 @@ pub fn panel(app: &mut FigApp, ui: &mut Ui) {
|
|||
} else if let Some(f) = app.ui.versions.form.as_mut().filter(|f| f.id.as_deref() == Some(row.id.as_str())) {
|
||||
inline_row(ui, i == last, |ui| form(ui, f, "Save", TEXT_X))
|
||||
} else {
|
||||
version_row(ui, row, i == last)
|
||||
version_row(ui, row, i == last, previewing.as_deref() == Some(row.id.as_str()))
|
||||
};
|
||||
if a.is_some() {
|
||||
action = a;
|
||||
|
|
@ -155,7 +161,16 @@ fn act(app: &mut FigApp, a: Action, rows: &[Row]) {
|
|||
let st = &mut app.ui.versions;
|
||||
let name_of = |id: &str| rows.iter().find(|r| r.id == id).map(|r| r.name.clone()).unwrap_or_default();
|
||||
match a {
|
||||
Action::Close => *st = VersionUi::default(),
|
||||
Action::Close => {
|
||||
*st = VersionUi::default();
|
||||
app.run("version.exitPreview", json!({}));
|
||||
}
|
||||
Action::Preview(id) => {
|
||||
app.run("version.preview", json!({"id": id}));
|
||||
}
|
||||
Action::ExitPreview => {
|
||||
app.run("version.exitPreview", json!({}));
|
||||
}
|
||||
Action::New => {
|
||||
st.confirm_delete = None;
|
||||
st.form = if st.form.as_ref().is_some_and(|f| f.id.is_none()) { None } else { Some(Form::new(None, "", "")) };
|
||||
|
|
@ -266,14 +281,21 @@ fn timeline(ui: &Ui, r: Rect, dot_y: f32, top: bool, bottom: bool, current: bool
|
|||
}
|
||||
}
|
||||
|
||||
fn current_row(ui: &mut Ui, more: bool) {
|
||||
/// The current version's row (`active` when it's what the canvas shows); true when clicked.
|
||||
fn current_row(ui: &mut Ui, more: bool, active: bool) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 36.0), Sense::hover());
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 36.0), Sense::click());
|
||||
if active {
|
||||
ui.painter().rect_filled(r, CornerRadius::ZERO, t.selected_row);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::ZERO, t.hover);
|
||||
}
|
||||
timeline(ui, r, r.center().y, false, more, true);
|
||||
ui.painter().text(pos2(r.min.x + TEXT_X, r.center().y), Align2::LEFT_CENTER, "Current version", semibold(11.0), t.text);
|
||||
!active && resp.on_hover_text("Back to the current version").clicked()
|
||||
}
|
||||
|
||||
fn version_row(ui: &mut Ui, row: &Row, last: bool) -> Option<Action> {
|
||||
fn version_row(ui: &mut Ui, row: &Row, last: bool, previewing: bool) -> Option<Action> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let w = ui.available_width();
|
||||
let text_w = (w - TEXT_X - 36.0).max(40.0);
|
||||
|
|
@ -286,7 +308,9 @@ fn version_row(ui: &mut Ui, row: &Row, last: bool) -> Option<Action> {
|
|||
let desc_h = desc.as_ref().map(|g| g.size().y + 4.0).unwrap_or(0.0);
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 50.0 + desc_h), Sense::click());
|
||||
let hovered = ui.rect_contains_pointer(r);
|
||||
if hovered {
|
||||
if previewing {
|
||||
ui.painter().rect_filled(r, CornerRadius::ZERO, t.selected_row);
|
||||
} else if hovered {
|
||||
ui.painter().rect_filled(r, CornerRadius::ZERO, t.hover);
|
||||
}
|
||||
let name_y = r.min.y + 17.0;
|
||||
|
|
@ -327,10 +351,65 @@ fn version_row(ui: &mut Ui, row: &Row, last: bool) -> Option<Action> {
|
|||
egui::Popup::context_menu(&resp).show(&mut menu);
|
||||
if resp.double_clicked() {
|
||||
out = Some(Action::Edit(row.id.clone()));
|
||||
} else if resp.clicked() && !previewing && out.is_none() {
|
||||
out = Some(Action::Preview(row.id.clone()));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// While a version is shown: a banner over the canvas naming it, with Restore and Back.
|
||||
pub fn preview_banner(app: &mut FigApp, ctx: &egui::Context, canvas: Rect) {
|
||||
let Some((id, name, time)) = app.session.ds().ok().and_then(|ds| ds.preview.as_ref().map(|p| (p.id.clone(), p.name.clone(), p.time))) else {
|
||||
return;
|
||||
};
|
||||
let t = Tokens::get(ctx);
|
||||
let mut action = None;
|
||||
egui::Area::new(Id::new("version_preview_banner"))
|
||||
.order(egui::Order::Foreground)
|
||||
.pivot(Align2::CENTER_TOP)
|
||||
.fixed_pos(pos2(canvas.center().x, canvas.min.y + 12.0))
|
||||
.show(ctx, |ui| {
|
||||
egui::Frame::NONE
|
||||
.fill(t.panel)
|
||||
.stroke(Stroke::new(1.0, t.border))
|
||||
.corner_radius(CornerRadius::same(8))
|
||||
.shadow(egui::epaint::Shadow { offset: [0, 2], blur: 12, spread: 0, color: t.shadow })
|
||||
.inner_margin(egui::Margin { left: 14, right: 8, top: 8, bottom: 8 })
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 8.0;
|
||||
icons::paint(
|
||||
ui.painter(),
|
||||
Rect::from_center_size(ui.cursor().min + vec2(7.0, 12.0), vec2(14.0, 14.0)),
|
||||
Icon::Clock,
|
||||
t.icon_secondary,
|
||||
);
|
||||
ui.add_space(20.0);
|
||||
ui.label(egui::RichText::new(format!("Viewing “{name}”")).font(semibold(11.0)).color(t.text));
|
||||
ui.label(egui::RichText::new(crate::comments::ago_long(time)).font(regular(11.0)).color(t.text_secondary));
|
||||
ui.add_space(4.0);
|
||||
if widgets::button(ui, "Back to current", false, true).clicked() {
|
||||
action = Some(false);
|
||||
}
|
||||
if widgets::button(ui, "Restore this version", true, true).clicked() {
|
||||
action = Some(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
match action {
|
||||
Some(true) => {
|
||||
if app.run("version.restore", json!({"id": id})).is_some() {
|
||||
app.toast(format!("Restored “{name}”"));
|
||||
}
|
||||
}
|
||||
Some(false) => {
|
||||
app.run("version.exitPreview", json!({}));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// A row holding a form (rename, delete confirmation), with the timeline running past it.
|
||||
fn inline_row(ui: &mut Ui, last: bool, add: impl FnOnce(&mut Ui) -> Option<Action>) -> Option<Action> {
|
||||
let top = ui.cursor().min.y;
|
||||
|
|
|
|||
|
|
@ -177,7 +177,11 @@ a new, renamed or deleted version marks the file as changed). Driven with `run_c
|
|||
versions are named "Version N"), `version.list` (oldest first; `time` is Unix seconds),
|
||||
`version.restore {id | index}` (replaces the document with the version as one undo step; the file
|
||||
name stays), `version.rename {id | index, name, description?}`, `version.delete {id | index}` and
|
||||
`version.duplicate {id | index}` (opens the version as a new document tab). Indexes are validated
|
||||
`version.duplicate {id | index}` (opens the version as a new document tab).
|
||||
`version.preview {id | index}` shows a version in place of the document, read-only: edits, undo and
|
||||
gestures are refused until `version.restore` keeps it or `version.exitPreview` goes back. Saving,
|
||||
closing or saving a version ends the preview first, and `version.list` marks the shown version
|
||||
`previewing`. Indexes are validated
|
||||
(a negative or out-of-range index is an error). A file keeps at most 100 versions; there are no
|
||||
automatic versions. `file.versionHistory` returns the same list and opens the panel in the app.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue