pub struct Form { /* private fields */ }Expand description
A parsed form file.
Holds the document rather than a value taken from it — comments, spacing and entry order included — because the designer edits this and saves it back, and a save that reformats what nobody touched is a save people learn not to make.
Implementations§
Source§impl Form
impl Form
Sourcepub fn build<M: Clone + 'static>(
&self,
ui: &mut Ui<M>,
parent: NodeId,
wiring: &mut impl Wiring<M>,
) -> Result<Built, Error>
pub fn build<M: Clone + 'static>( &self, ui: &mut Ui<M>, parent: NodeId, wiring: &mut impl Wiring<M>, ) -> Result<Built, Error>
#[derive(Clone, Copy, PartialEq, Debug)]
enum Message {
Greet,
}
let form = Form::parse(
r#"form "Hello" version=1 width=320 height=120 { button "Greet" name=go x=8 y=8 w=90 h=30 on-press=greet }"#,
)?;
let mut ui: Ui<Message> = Ui::new(form.size(), form.theme());
let root = ui.root();
// The one thing a file cannot hold: this application's own message type.
let built = form.build(&mut ui, root, &mut |name: &str, payload: Payload| {
match (name, payload) {
("greet", Payload::None) => Some(Handler::Plain(Message::Greet)),
_ => None,
}
})?;
// What the file named, by the name it used.
let button = built.node("go").expect("the form names it `go`");
assert_eq!(built.len(), 1);
assert!(!built.is_empty());
// And everything it put on screen, named or not, in file order.
assert_eq!(built.placed().len(), 1);
assert_eq!(built.at(&[0]).map(|node| node.kind), Some("button"));
assert_eq!(built.at(&[0]).map(|node| node.id), Some(button));
assert_eq!(
built.names().map(|(name, _)| name).collect::<Vec<_>>(),
vec!["go"],
);Builds this form into ui under parent.
Nodes are added in file order, so paint order is file order. See the
crate documentation for what wiring supplies and why.
§Errors
Every failure carries a line and a column. See Reason
for the whole list.
Sourcepub fn build_scaled<M: Clone + 'static>(
&self,
ui: &mut Ui<M>,
parent: NodeId,
scale: f32,
wiring: &mut impl Wiring<M>,
) -> Result<Built, Error>
pub fn build_scaled<M: Clone + 'static>( &self, ui: &mut Ui<M>, parent: NodeId, scale: f32, wiring: &mut impl Wiring<M>, ) -> Result<Built, Error>
Builds this form at scale: every rectangle and every length in it
multiplied once, on the way in.
The DPI answer this toolkit gives, for a form. An application computing
its own rectangles multiplies them itself — three lines, and
examples/hello is those three lines. A form file has no application
doing that, so the multiplying goes where the rectangles are computed,
which is here.
Two things the caller still has to do, because neither belongs to a subtree:
// The theme's metrics, or every widget is the old size inside a new
// rectangle — a 2x button with a 6px corner on it.
let mut ui: Ui<Void> = Ui::new(Size::new(1920, 1080), form.theme().scaled(scale));…and putting the form where it goes, which is Form::fit.
Text scales like a rectangle here, and that is a choice. A 1024x600 form on a 1920x1080 panel gets 16 px text at 30 px. That is right when the panel is the same screen at a higher density and wrong when it is a bigger screen meant to show more. This does the first one. The second is not a multiplication and no file can express it.
§Errors
The same as Form::build; scaling adds no failure of its own.
Sourcepub fn build_with_design<M: Clone + 'static>(
&self,
ui: &mut Ui<M>,
parent: NodeId,
scale: f32,
wiring: &mut impl Wiring<M>,
) -> Result<Built, Error>
pub fn build_with_design<M: Clone + 'static>( &self, ui: &mut Ui<M>, parent: NodeId, scale: f32, wiring: &mut impl Wiring<M>, ) -> Result<Built, Error>
Builds the form and the placeholder content a designer needs to see.
Every other build skips design { … } blocks, so a table comes up with
its columns and no rows and a timeline with no events: those are the
application’s to supply, and a kiosk should not carry four names somebody
typed to make a canvas look right. A designer is the one caller that
wants them, because a table drawn with no rows is not a table anybody can
lay out against.
scale is Form::build_scaled’s, so a designer’s canvas magnifies the
same way.
let source = r#"
form "F" version=1 width=200 height=80 {
table name=t x=0 y=0 w=200 h=80 {
column "Name"
design {
row "Ada"
}
}
}
"#;
let form = Form::parse(source)?;
let mut wiring = |_: &str, _: Payload| None::<Handler<Void>>;
// What ships: the column, and no rows at all.
let mut ui: Ui<Void> = Ui::new(form.size(), form.theme());
let root = ui.root();
form.build(&mut ui, root, &mut wiring)?;
// What the designer draws.
let mut canvas: Ui<Void> = Ui::new(form.size(), form.theme());
let root = canvas.root();
form.build_with_design(&mut canvas, root, 1.0, &mut wiring)?;Sourcepub fn build_fitted<M: Clone + 'static>(
&self,
ui: &mut Ui<M>,
parent: NodeId,
fit: Placement,
wiring: &mut impl Wiring<M>,
) -> Result<Built, Error>
pub fn build_fitted<M: Clone + 'static>( &self, ui: &mut Ui<M>, parent: NodeId, fit: Placement, wiring: &mut impl Wiring<M>, ) -> Result<Built, Error>
Builds this form at a Fit — a factor per axis, which is what
Scaling::Stretch needs and Form::fit works
out.
Only Placement::x and Placement::y are read. Placement::rect is where the
caller puts the node this builds into, and is none of this method’s
business: a form is built under whatever parent it is given.
let form = Form::parse(
r#"form "F" version=1 width=200 height=100 scaling=proportional {
label "Hi" name=hi x=10 y=10 w=100 h=20 size=16
}"#,
)?;
let surface = Size::new(400, 400);
let fit = form.fit(surface);
// The theme is scaled once, here, and the form is built into a panel at
// the rectangle the fit worked out.
let mut ui: Ui<Void> = Ui::new(surface, form.theme().scaled(fit.uniform()));
let root = ui.root();
let stage = ui.add(root, Panel::filled(form.background()), fit.rect).unwrap();
let mut nothing = |_: &str, _: denise_forms::Payload| None;
let built = form.build_fitted(&mut ui, stage, fit, &mut nothing)?;
let hi = built.node("hi").expect("the form names it");
assert_eq!(ui.layout(hi), Some(Rect::new(20, 20, 200, 40)), "twice as big");
assert_eq!(
ui.get_property(hi, "size"),
Some(denise_ui::widgets::Value::Int(32)),
"and so is the text",
);§Errors
The same as Form::build.
Source§impl Form
impl Form
Sourcepub fn parse_within(source: &str, limit: Duration) -> Result<Self, Error>
pub fn parse_within(source: &str, limit: Duration) -> Result<Self, Error>
Parses a form, giving up after limit.
Otherwise exactly Form::parse — same document, same errors, same
byte-for-byte round trip — with two more ways to fail:
Reason::TooSlow when the deadline passes, and Reason::NoThread
when the parse could not be started at all.
Use this for any form the program did not write: opened by a person,
pasted, downloaded, handed over on a stick, or watched on disk while a
text editor has it too. A form compiled in with include_str! is read at
build time from a file in the repository and needs nothing from here.
The call returns within limit plus the cost of spawning a thread. What
it does not do is stop the parse. A thread cannot be cancelled and
kdl has no point at which to ask it to stop, so an overrun is
abandoned: this returns, and the worker keeps parsing until it finishes,
which for the exponential shapes may be never. That bounds the call and
not the process, which is what MAX_ABANDONED is for.
let source = "form \"F\" version=1 width=64 height=32 {\n\
\x20 label \"Hello\" x=0 y=0 w=64 h=16\n\
}\n";
let form = Form::parse_within(source, PATIENCE).expect("a form, in time");
assert_eq!(form.text(), source);Source§impl Form
impl Form
Sourcepub fn parse(source: &str) -> Result<Self, Error>
pub fn parse(source: &str) -> Result<Self, Error>
Parses a form file.
The source is kept, and what is kept is checked: the parsed document is
written back out and compared to the input, so a file that would not
reproduce is refused with Reason::NotPreserved rather than accepted
and corrupted on the first save.
This is bounded in shape and not in time: MAX_SOURCE,
MAX_DEPTH, MAX_COMMENTED_DEPTH and a brace count refuse every
slow file anybody has found, and kdl has exponential corners nobody
has found yet. For a form this program did not write, use
Form::parse_within, which is this with a clock.
let form = Form::parse(r#"
form "Hello" version=1 kind=screen width=460 height=260
"#)?;
assert_eq!(form.title(), "Hello");
assert_eq!(form.kind(), FormKind::Screen);
assert_eq!(form.size(), denise::Size::new(460, 260));Sourcepub fn title(&self) -> &str
pub fn title(&self) -> &str
What a form says about itself, and the defaults for what it does not say.
let form = Form::parse(
r#"form "Preferences" name=prefs version=1 kind=window width=520 height=340 theme=light background=base-200"#,
)?;
assert_eq!(form.title(), "Preferences");
assert_eq!(form.name(), Some("prefs"));
assert_eq!(form.version(), 1);
assert_eq!(form.kind(), FormKind::Window);
assert_eq!(form.size(), denise::Size::new(520, 340));
assert_eq!(form.theme_name(), "light");
assert_eq!(form.background(), denise::Role::Base200);
assert_eq!(form.theme(), denise::theme::LIGHT);
// Nothing written is the default: a window may be resized, and says
// nothing about a smallest size.
assert!(form.resizable());
assert_eq!(form.min_size(), None);The form’s title — a window’s title bar, and the designer’s name for it.
Sourcepub fn name(&self) -> Option<&str>
pub fn name(&self) -> Option<&str>
See Form::title for what a form says about itself.
The form’s identifier, if it was given one.
Sourcepub fn version(&self) -> u64
pub fn version(&self) -> u64
See Form::title for what a form says about itself.
The schema version the file declares.
Sourcepub fn kind(&self) -> FormKind
pub fn kind(&self) -> FormKind
See Form::title for what a form says about itself.
What this form is for. FormKind::Screen unless the file says otherwise.
Sourcepub fn scaling(&self) -> Scaling
pub fn scaling(&self) -> Scaling
See Form::title for what a form says about itself.
Whether this form consents to being drawn at another size.
Scaling::None unless the file says otherwise, because that is what
every form written before the property existed already did.
Sourcepub fn fit(&self, surface: Size) -> Placement
pub fn fit(&self, surface: Size) -> Placement
How this form occupies a surface of some other size.
Reads Form::scaling and does the arithmetic, so that the policy lives
in the file and the multiplication lives here — rather than in every
application that loads a form.
let source = |scaling: &str| {
format!(r#"form "F" version=1 width=200 height=100 scaling={scaling} {{ }}"#)
};
// The default: its own size, in the middle of the surface.
let fixed = Form::parse(&source("none"))?;
let fit = fixed.fit(Size::new(400, 400));
assert_eq!((fit.x, fit.y), (1.0, 1.0));
assert_eq!(fit.rect, Rect::new(100, 150, 200, 100));
// Proportional: as big as fits, letterboxed on the axis with room left.
let fits = Form::parse(&source("proportional"))?;
let fit = fits.fit(Size::new(400, 400));
assert_eq!((fit.x, fit.y), (2.0, 2.0), "the tighter axis decides");
assert_eq!(fit.rect, Rect::new(0, 100, 400, 200));
// Stretch: the whole surface, whatever that does to the shape.
let fills = Form::parse(&source("stretch"))?;
let fit = fills.fit(Size::new(400, 400));
assert_eq!((fit.x, fit.y), (2.0, 4.0));
assert_eq!(fit.rect, Rect::from_size(Size::new(400, 400)));Sourcepub fn resizable(&self) -> bool
pub fn resizable(&self) -> bool
let fixed = Form::parse(
r#"form "F" version=1 kind=window width=400 height=300 resizable=#false min-width=320 min-height=240"#,
)?;
assert!(!fixed.resizable());
assert_eq!(fixed.min_size(), Some(denise::Size::new(320, 240)));Whether a window form may be resized. true unless the file says not.
Meaningless on any other kind, which is why the file is not allowed to say it on one.
Sourcepub fn min_size(&self) -> Option<Size>
pub fn min_size(&self) -> Option<Size>
See Form::resizable.
The smallest a window form may be made, if it says.
Sourcepub fn dim(&self) -> u8
pub fn dim(&self) -> u8
let asked = Form::parse(r#"form "F" version=1 kind=dialog width=380 height=170 dim=200"#)?;
assert_eq!(asked.dim(), 200);
let quiet = Form::parse(r#"form "F" version=1 kind=dialog width=380 height=170"#)?;
assert_eq!(quiet.dim(), 160);How dark the backdrop behind a dialog is, 0 to 255. 160 by default,
which is what denise_ui::Ui::push_scene is usually given.
Sourcepub fn side(&self) -> Side
pub fn side(&self) -> Side
width and height are the surface it comes in over; extent is
how far it comes in, and across the other axis it covers the surface.
let drawer = Form::parse(r#"form "F" version=1 kind=drawer width=1024 height=600 extent=320"#)?;
assert_eq!(drawer.side(), Side::Before);
assert_eq!(drawer.extent(), 320);
// A shelf is a bar rather than a side panel, so it comes in from below.
let shelf = Form::parse(r#"form "F" version=1 kind=shelf width=1024 height=600 extent=180"#)?;
assert_eq!(shelf.side(), Side::Below);Which edge a drawer or a shelf comes in from.
The defaults differ by kind and deliberately: a drawer is a side panel and a shelf is a bar, so they come in from different edges when nobody says.
Sourcepub fn extent(&self) -> i32
pub fn extent(&self) -> i32
See Form::side.
How far a drawer or a shelf comes in, in logical pixels.
Required on those two kinds, so this is what the file says or 0 on a
kind that has no such thing.
Sourcepub fn size(&self) -> Size
pub fn size(&self) -> Size
See Form::title for what a form says about itself.
The size the form was designed at, in logical pixels.
Sourcepub fn theme(&self) -> Theme
pub fn theme(&self) -> Theme
See Form::title for what a form says about itself.
The theme the file names, or the dark one.
Sourcepub fn theme_name(&self) -> &str
pub fn theme_name(&self) -> &str
See Form::title for what a form says about itself.
The theme’s name, as the file spells it.
Sourcepub fn background(&self) -> Role
pub fn background(&self) -> Role
See Form::title for what a form says about itself.
The surface the form is drawn on.
Sourcepub fn text(&self) -> String
pub fn text(&self) -> String
The file as it now stands.
Byte for byte what was parsed, until something edits it, and then byte for
byte what was parsed apart from what was edited. kdl holds the
document rather than a value taken from it, so comments, blank lines,
column alignment and entry order all survive an edit to a property three
nodes away. That is the round trip the designer stands on, and the reason
this crate parses the way it does.
// A comment, a blank line, and columns somebody lined up by hand.
let source = "\
// The panel everything sits on.
form \"F\" version=1 width=320 height=240 {
label \"One\" x=8 y=8 w=80 h=20
label \"Two\" x=8 y=32 w=80 h=20
}
";
let mut form = Form::parse(source)?;
assert_eq!(form.text(), source, "parsing changed nothing");
// One number, one line: everything else is where it was, spacing and all.
form.apply(Edit::number(&[1], "y", Some(40)))?;
assert_eq!(
form.text(),
source.replace("x=8 y=32", "x=8 y=40"),
);Sourcepub fn set_number(&mut self, path: &[usize], name: &str, value: i64) -> bool
pub fn set_number(&mut self, path: &[usize], name: &str, value: i64) -> bool
Sets a whole-number property on the node at path.
Replaces the value in place when the property is already there, which is what keeps a move to a one-line diff: everything else on the line, and every line around it, is untouched. Appends when it is not.
Returns false if there is no node at that path.
let mut form = Form::parse(
"form \"F\" version=1 width=99 height=99 {\n \
label \"hi\" x=10 y=20 w=30 h=40 // where it sits\n}\n",
)?;
assert!(form.set_number(&[0], "x", 25));
assert!(form.text().contains("x=25 y=20"));
assert!(form.text().contains("// where it sits"), "the comment survived");Sourcepub fn property(&self, path: &[usize], name: &str) -> Option<String>
pub fn property(&self, path: &[usize], name: &str) -> Option<String>
What the file writes for a node’s property, or None when it does not
write it at all.
The value, not the spelling: a string comes back unquoted, because an
inspector’s field edits the string and not the quotes around it. What a
None means is the whole of “this property is at its default” — the
schema does not write a default, so nothing written is the default.
let form = Form::parse(r#"form "F" version=1 width=99 height=99 { label "Hi" name=greeting x=8 y=8 w=80 h=20 }"#)?;
assert_eq!(form.property(&[0], "x").as_deref(), Some("8"));
// Unquoted, because a field edits the string and not the quotes.
assert_eq!(form.property(&[0], "name").as_deref(), Some("greeting"));
// Not written is the default.
assert_eq!(form.property(&[0], "role"), None);Sourcepub fn items(&self, path: &[usize], kind: &str) -> Vec<String>
pub fn items(&self, path: &[usize], kind: &str) -> Vec<String>
The arguments of a node’s children of one kind, in file order.
What a collection holds: a select’s options, a tabs’s tabs, a
table’s columns. Each item is the child’s own argument, which is how
every collection in this format writes its text.
Named by the child node rather than by a plural, because that is what the
file says and what PropertyKind::List
names: a property called option is the option nodes under it.
let form = Form::parse(
"form \"F\" version=1 width=99 height=99 {\n select name=job x=0 y=0 w=9 h=9 {\n option \"Reader\"\n option \"Author\"\n }\n}\n",
)?;
assert_eq!(form.items(&[0], "option"), ["Reader", "Author"]);
// A kind the node does not hold, and a node that is not there.
assert!(form.items(&[0], "tab").is_empty());
assert!(form.items(&[9], "option").is_empty());Sourcepub fn child_count(&self, path: &[usize]) -> usize
pub fn child_count(&self, path: &[usize]) -> usize
How many children a node has, of every kind.
Where an appended child goes. Not the same as items(path, kind).len():
a table holds columns and rows, so the index among one kind is
not the index among children — which is the index every edit takes.
let form = Form::parse(
"form \"F\" version=1 width=99 height=99 {\n table name=t x=0 y=0 w=9 h=9 {\n column \"A\"\n column \"B\"\n design {\n row \"1\"\n row \"2\"\n }\n }\n}\n",
)?;
// Two columns and the `design` block, which is a child like any other.
assert_eq!(form.child_count(&[0]), 3);
assert_eq!(form.items(&[0], "row").len(), 2);
assert_eq!(form.child_count(&[9]), 0);Sourcepub fn item_path(
&self,
path: &[usize],
kind: &str,
nth: usize,
) -> Option<Vec<usize>>
pub fn item_path( &self, path: &[usize], kind: &str, nth: usize, ) -> Option<Vec<usize>>
Where a node’s nth child of one kind sits, for an edit that means it.
A collection’s items are addressed like any other node — see
Edit::Argument, Edit::Insert, Edit::Remove and
Edit::Move, all of which already reach them — but the index among
options is not the index among children when a node holds more than
one kind. This translates.
let form = Form::parse(
"form \"F\" version=1 width=99 height=99 {\n table name=t x=0 y=0 w=9 h=9 {\n column \"A\"\n column \"B\"\n design {\n row \"1\"\n row \"2\"\n }\n }\n}\n",
)?;
// The second `row` is the second child of the table's third child,
// because placeholder content lives in `design`.
assert_eq!(form.item_path(&[0], "row", 1), Some(vec![0, 2, 1]));
// Real content is addressed where it is written.
assert_eq!(form.item_path(&[0], "column", 1), Some(vec![0, 1]));
// Past the end, and a kind the node does not hold.
assert_eq!(form.item_path(&[0], "row", 2), None);
assert_eq!(form.item_path(&[0], "option", 0), None);Sourcepub fn collection_parent(
&self,
path: &[usize],
kind: &str,
) -> Option<Vec<usize>>
pub fn collection_parent( &self, path: &[usize], kind: &str, ) -> Option<Vec<usize>>
The node an item of kind is written under, as a path.
The node at path itself for real content — a table’s columns are
its own children. Its design { … } block for placeholder content, which
is where a row goes so that no build but a designer’s loads it.
None when the node has no design block yet, which is the caller’s cue
to write one: the first row a designer adds brings the block with it.
let form = Form::parse(r#"
form "F" version=1 width=99 height=99 {
table name=t x=0 y=0 w=99 h=99 {
column "Name"
}
}
"#)?;
// A column is written on the table.
assert_eq!(form.collection_parent(&[0], "column"), Some(vec![0]));
// A row would need a `design` block, and there is none.
assert_eq!(form.collection_parent(&[0], "row"), None);Sourcepub fn node_text(&self, path: &[usize]) -> Option<String>
pub fn node_text(&self, path: &[usize]) -> Option<String>
The source of one node, as it stands in the file, with its own indentation taken off.
What copying a node puts on the clipboard: .dform source that reads as
source. Its children come with it, and so does a comment written above
it — the node’s leading trivia is part of the node, which is the same
reason an undone removal puts the comment back.
let form = Form::parse(
"form \"F\" version=1 width=99 height=99 {\n panel name=box x=0 y=0 w=9 h=9 {\n label \"in\" x=1 y=1 w=2 h=2\n }\n}\n",
)?;
assert_eq!(
form.node_text(&[0]).as_deref(),
Some("panel name=box x=0 y=0 w=9 h=9 {\n label \"in\" x=1 y=1 w=2 h=2\n}\n"),
);
assert_eq!(form.node_text(&[9]), None);Sourcepub fn written(&self) -> Vec<Written>
pub fn written(&self) -> Vec<Written>
Every node in the file, depth first, the form node itself first.
What can be known about a form without building it, which is what
comparing two versions of the same file needs: Placed
is the same
node after build and carries a NodeId that only exists
once there is a tree, so it cannot describe a file nobody has opened.
let form = Form::parse(
"form \"F\" version=1 width=99 height=99 {\n panel name=box x=0 y=0 w=9 h=9 {\n label \"in\" x=1 y=1 w=2 h=2\n }\n}\n",
)?;
let written = form.written();
let kinds: Vec<&str> = written.iter().map(|node| node.kind.as_str()).collect();
assert_eq!(kinds, ["form", "panel", "label"]);
assert_eq!(written[1].name.as_deref(), Some("box"));
assert_eq!(written[2].argument.as_deref(), Some("in"));
assert_eq!(written[2].path, vec![0, 0]);
// The node itself, without the children indented under it.
assert_eq!(written[1].line, "panel name=\"box\" x=0 y=0 w=9 h=9");Sourcepub fn argument(&self, path: &[usize]) -> Option<String>
pub fn argument(&self, path: &[usize]) -> Option<String>
let form = Form::parse(r#"form "F" version=1 width=99 height=99 { label "Hello" x=0 y=0 w=9 h=9 }"#)?;
assert_eq!(form.argument(&[0]).as_deref(), Some("Hello"));
// The form's own argument is its title.
assert_eq!(form.argument(&[]).as_deref(), Some("F"));A node’s positional argument — the "Hello" in label "Hello".
Sourcepub fn clear_property(&mut self, path: &[usize], name: &str) -> bool
pub fn clear_property(&mut self, path: &[usize], name: &str) -> bool
Removes a property from the node at path.
What a designer does when a property goes back to its default: the schema says a default is not written, so resetting one is deleting it rather than spelling it out.
let mut form = Form::parse(r#"form "F" version=1 width=99 height=99 { label "Hi" x=0 y=0 w=9 h=9 role=primary }"#)?;
assert!(form.clear_property(&[0], "role"));
assert_eq!(form.property(&[0], "role"), None);
// Nothing there to clear.
assert!(!form.clear_property(&[0], "role"));Use Form::apply with Edit::property instead where the change has
to be undoable: this one hands back nothing to put it back with.
Sourcepub fn remove_at(&mut self, path: &[usize]) -> bool
pub fn remove_at(&mut self, path: &[usize]) -> bool
Removes the node at path, and everything under it.
Returns false if there is no node there.
let mut form = Form::parse(r#"form "F" version=1 width=99 height=99 { label "Hi" x=0 y=0 w=9 h=9 }"#)?;
assert!(form.remove_at(&[0]));
assert!(!form.text().contains("label"));
assert!(!form.remove_at(&[0]), "there is nothing there now");Use Form::apply with Edit::remove instead where the change has to
be undoable.
Sourcepub fn apply(&mut self, edit: Edit) -> Result<Edit, Error>
pub fn apply(&mut self, edit: Edit) -> Result<Edit, Error>
Applies an edit, and hands back the edit that undoes it.
The whole of undo, and the reason it is exact: because this crate holds the document rather than a value taken from it, the inverse of an edit is knowable at the moment it is made and is itself an ordinary edit. There is no snapshot of anything — a stack of these is a stack of small, reversible facts.
let source = "form \"F\" version=1 width=9 height=9 {\n \
label \"hi\" x=1 y=2 w=3 h=4 // a note\n}\n";
let mut form = Form::parse(source)?;
let undo = form.apply(Edit::number(&[0], "x", Some(40)))?;
assert!(form.text().contains("x=40"));
form.apply(undo)?;
assert_eq!(form.text(), source, "byte for byte, comment and all");§Errors
When the path names no node, when the text of an insertion is not one node, or when a property being replaced holds something other than a whole number — which could not be put back, and so is refused rather than silently made irreversible.