Repository: zng-ui/zng (language: rust)

Issue:
Implement screenshot function in the Inspector
This is a good feature to have and we needed it to take example screenshots.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 890ed38a4..df74f018e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,5 @@ TODO/_local.md
 zng-view-prebuilt/lib/zng_view.*
 zng-view-prebuilt/lib/libzng_view.*
 **/*.mm_profdata
-*.mono_items.md
\ No newline at end of file
+*.mono_items.md
+examples/.do-doc-readme-*
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20f23e308..fd618b7f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
 # Unpublished
 
 * Add Cargo feature documentation in each crate `README.md` and `lib.rs` docs.
+* Add Screenshot function to the Inspector window.
+* Fix `formatx!` causing futures to not be Send+Sync.
+* `UiTask` now logs a warning if dropped while pending.
+* Add `UiTask::cancel` to drop a pending task without logging a warning.
+* Fix `WINDOWS.frame_image` capture with multiple windows capturing pixels from the wrong window.
+* Fix `WINDOWS.frame_image` var not updating on load or error.
 
 # 0.3.3
 
diff --git a/docs/Examples.md b/docs/Examples.md
new file mode 100644
index 000000000..fc04beb16
--- /dev/null
+++ b/docs/Examples.md
@@ -0,0 +1,69 @@
+# Examples
+
+See the [examples README](../examples/README.md) for information about the current examples. This document is for example contributors.
+
+## Adding an Example
+
+Add the new example in `examples/<example-name>.rs`:
+
+```rust
+//! Demonstrates foo, bar.
+
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+use zng::prelude::*;
+
+use zng::view_process::prebuilt as view_process;
+
+fn main() {
+    examples_util::print_info();
+    // view_process::run_same_process(app_main);
+
+    view_process::init();
+    app_main();
+}
+
+fn app_main() {
+    APP.defaults().run_window(async {
+        Window! {
+            title = "Foo Example";
+            child = Text!("Bar");
+        }
+    })
+}
+```
+
+Register it in `examples/Cargo.toml`:
+
+```toml
+[[example]]
+name = "<example-name>"
+path = "<example-name>.rs"
+```
+
+Run the example and test it.
+
+```console
+cargo do run <example-name>
+```
+
+Optionally, take a screenshot and save it to `examples/res/screenshots/<example-name>.png`. You can take a screenshot using
+the inspector window, press `Ctrl+Shift+I` then click the "Save Screenshot" menu.
+
+Run [`oxipng`](https://github.com/shssoichiro/oxipng) or another minifier on the screenshot before committing.
+
+```console
+oxipng -o max --strip safe --alpha "examples/res/screenshots/<example-name>.png"
+```
+
+Update the auto generated README:
+
+```console
+cargo do doc --readme-examples
+```
+
+Done. You can run the new example using:
+
+```console
+cargo do run <example-name>
+```
diff --git a/examples/Cargo.toml b/examples/Cargo.toml
index 537ff69cd..2e1f5f1a9 100644
--- a/examples/Cargo.toml
+++ b/examples/Cargo.toml
@@ -104,8 +104,8 @@ name = "scroll"
 path = "scroll.rs"
 
 [[example]]
-name = "shortcuts"
-path = "shortcuts.rs"
+name = "shortcut"
+path = "shortcut.rs"
 
 [[example]]
 name = "text"
diff --git a/examples/README.md b/examples/README.md
index 511ed7847..5aa3e06b8 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,51 +1,262 @@
-# Examples
 
-This directory contains small example apps.
+<!--do doc --readme-examples-->
+### `animation`
 
-# Running
+<img alt='headless screenshot' src='./res/screenshots/animation.png' width='300'>
 
-To run an example use `cargo do run $name`.
+Source: [animation.rs](./animation.rs)
 
-# Adding an Example
+```console
+cargo do run animation
+```
+
+Demonstrates animation, easing functions.
 
-To add an example, create a file then add it in `./Cargo.toml`.
+### `border`
 
-## Template
+<img alt='headless screenshot' src='./res/screenshots/border.png' width='300'>
 
-This is a good example template:
+Source: [border.rs](./border.rs)
 
-In `./foo.rs`: 
-```rust
-#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+```console
+cargo do run border
+```
 
-use zng::prelude::*;
+Demonstrates borders, corner radius, multiple borders per widget and clip-to-bounds.
 
-use zng::view_process::prebuilt as view_process;
+### `button`
 
-fn main() {
-    examples_util::print_info();
-    // view_process::run_same_process(app_main);
+<img alt='headless screenshot' src='./res/screenshots/button.png' width='300'>
 
-    view_process::init();
-    app_main();
-}
+Source: [button.rs](./button.rs)
 
-fn app_main() {
-    APP.defaults().run_window(async {
-        Window! {
-            title = "Foo Example";
-            child = Text!("Bar");
-        }
-    })
-}
+```console
+cargo do run button
 ```
 
-Then add in `./Cargo.toml`:
+Demonstrates the button widget.
+
+### `calculator`
 
-```toml
-[[example]]
-name = "foo"
-path = "foo.rs"
+<img alt='headless screenshot' src='./res/screenshots/calculator.png' width='300'>
+
+Source: [calculator.rs](./calculator.rs)
+
+```console
+cargo do run calculator
 ```
 
-Then run from the project root using `cargo do run foo`.
\ No newline at end of file
+Simple calculator, demonstrates Grid layout, data context.
+
+### `config`
+
+<img alt='headless screenshot' src='./res/screenshots/config.png' width='300'>
+
+Source: [config.rs](./config.rs)
+
+```console
+cargo do run config
+```
+
+Demonstrates the CONFIG service, live updating config between processes.
+
+### `countdown`
+
+Source: [countdown.rs](./countdown.rs)
+
+```console
+cargo do run countdown
+```
+
+Demonstrates the TIMERS service, variable mapping.
+
+### `cursor`
+
+<img alt='headless screenshot' src='./res/screenshots/cursor.png' width='300'>
+
+Source: [cursor.rs](./cursor.rs)
+
+```console
+cargo do run cursor
+```
+
+Demonstrates each `CursorIcon`, tooltip anchored to cursor.
+
+### `extend_view`
+
+Source: [extend_view.rs](./extend_view.rs)
+
+```console
+cargo do run extend_view
+```
+
+Demonstrates the `zng-view` extension API and render extensions API.
+
+### `focus`
+
+<img alt='headless screenshot' src='./res/screenshots/focus.png' width='300'>
+
+Source: [focus.rs](./focus.rs)
+
+```console
+cargo do run focus
+```
+
+Demonstrates the focus service, logical and directional navigation.
+
+### `gradient`
+
+<img alt='headless screenshot' src='./res/screenshots/gradient.png' width='300'>
+
+Source: [gradient.rs](./gradient.rs)
+
+```console
+cargo do run gradient
+```
+
+Demonstrates gradient rendering.
+
+### `headless`
+
+<img alt='headless screenshot' src='./res/screenshots/headless.png' width='300'>
+
+Source: [headless.rs](./headless.rs)
+
+```console
+cargo do run headless
+```
+
+Demonstrates headless apps, image and video rendering.
+
+### `icon`
+
+<img alt='headless screenshot' src='./res/screenshots/icon.png' width='300'>
+
+Source: [icon.rs](./icon.rs)
+
+```console
+cargo do run icon
+```
+
+Search and copy Material Icons constants.
+
+### `image`
+
+<img alt='headless screenshot' src='./res/screenshots/image.png' width='300'>
+
+Source: [image.rs](./image.rs)
+
+```console
+cargo do run image
+```
+
+Demonstrates image loading, displaying, animated sprites, rendering, pasting.
+
+### `layer`
+
+<img alt='headless screenshot' src='./res/screenshots/layer.png' width='300'>
+
+Source: [layer.rs](./layer.rs)
+
+```console
+cargo do run layer
+```
+
+Demonstrates the LAYERS service.
+
+### `localize`
+
+<img alt='headless screenshot' src='./res/screenshots/localize.png' width='300'>
+
+Source: [localize.rs](./localize.rs)
+
+```console
+cargo do run localize
+```
+
+Demonstrates localization.
+
+### `markdown`
+
+<img alt='headless screenshot' src='./res/screenshots/markdown.png' width='300'>
+
+Source: [markdown.rs](./markdown.rs)
+
+```console
+cargo do run markdown
+```
+
+Demonstrates the `Markdown!` widget.
+
+### `respawn`
+
+<img alt='headless screenshot' src='./res/screenshots/respawn.png' width='300'>
+
+Source: [respawn.rs](./respawn.rs)
+
+```console
+cargo do run respawn
+```
+
+Demonstrates the view-process respawn error recovery feature.
+
+### `scroll`
+
+<img alt='headless screenshot' src='./res/screenshots/scroll.png' width='300'>
+
+Source: [scroll.rs](./scroll.rs)
+
+```console
+cargo do run scroll
+```
+
+Demonstrates the `Scroll!` widget and scroll commands.
+
+### `shortcut`
+
+<img alt='headless screenshot' src='./res/screenshots/shortcut.png' width='300'>
+
+Source: [shortcut.rs](./shortcut.rs)
+
+```console
+cargo do run shortcut
+```
+
+Small utility that displays the pressed key gestures.
+
+### `text`
+
+<img alt='headless screenshot' src='./res/screenshots/text.png' width='300'>
+
+Source: [text.rs](./text.rs)
+
+```console
+cargo do run text
+```
+
+Demonstrates the `Text!` and `TextInput!` widgets. Text rendering, text editor.
+
+### `transform`
+
+<img alt='headless screenshot' src='./res/screenshots/transform.png' width='300'>
+
+Source: [transform.rs](./transform.rs)
+
+```console
+cargo do run transform
+```
+
+Demonstrates 2D and 3D transforms, touch transforms.
+
+### `window`
+
+<img alt='headless screenshot' src='./res/screenshots/window.png' width='300'>
+
+Source: [window.rs](./window.rs)
+
+```console
+cargo do run window
+```
+
+Demonstrates the window widget, service, state and commands.
+
diff --git a/examples/animation.rs b/examples/animation.rs
index 4d29ae560..f8108112b 100644
--- a/examples/animation.rs
+++ b/examples/animation.rs
@@ -1,3 +1,5 @@
+//! Demonstrates animation, easing functions.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use std::collections::HashMap;
diff --git a/examples/border.rs b/examples/border.rs
index 8f0b2f2a2..9fba4849a 100644
--- a/examples/border.rs
+++ b/examples/border.rs
@@ -1,3 +1,5 @@
+//! Demonstrates borders, corner radius, multiple borders per widget and clip-to-bounds.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 use zng::{
     prelude::*,
diff --git a/examples/button.rs b/examples/button.rs
index ae0e5abb7..df1ca3fac 100644
--- a/examples/button.rs
+++ b/examples/button.rs
@@ -1,3 +1,5 @@
+//! Demonstrates the button widget.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 use zng::{
     color::filter::opacity,
diff --git a/examples/calculator.rs b/examples/calculator.rs
index ba38f1fb0..929d6eb42 100644
--- a/examples/calculator.rs
+++ b/examples/calculator.rs
@@ -1,3 +1,5 @@
+//! Simple calculator, demonstrates Grid layout, data context.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 use std::convert::TryInto;
 use zng::{
diff --git a/examples/config.rs b/examples/config.rs
index 998c4dedf..881cb0197 100644
--- a/examples/config.rs
+++ b/examples/config.rs
@@ -1,3 +1,5 @@
+//! Demonstrates the CONFIG service, live updating config between processes.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 use zng::{
     color::filter::opacity,
@@ -122,6 +124,7 @@ fn app_main() {
 
         Window! {
             title = if std::env::var("MOVE-TO").is_err() { "Config Example" } else { "Config Example - Other Process" };
+            size = (600, 500);
             widget::background = Text! {
                 txt = CONFIG.status().map_to_txt();
                 margin = 10;
diff --git a/examples/countdown.rs b/examples/countdown.rs
index da5102135..4543c9083 100644
--- a/examples/countdown.rs
+++ b/examples/countdown.rs
@@ -1,3 +1,5 @@
+//! Demonstrates the TIMERS service, variable mapping.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 use zng::{image, prelude::*, widget::background_color};
 
diff --git a/examples/cursor.rs b/examples/cursor.rs
index db97c471b..a1cdb4a3d 100644
--- a/examples/cursor.rs
+++ b/examples/cursor.rs
@@ -1,3 +1,5 @@
+//! Demonstrates each `CursorIcon`, tooltip anchored to cursor.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use zng::{
diff --git a/examples/extend_view.rs b/examples/extend_view.rs
index eb5c9ebd0..218b28e04 100644
--- a/examples/extend_view.rs
+++ b/examples/extend_view.rs
@@ -1,3 +1,5 @@
+//! Demonstrates the `zng-view` extension API and render extensions API.
+
 use zng::{color::filter::hue_rotate, layout::size, prelude::*};
 use zng_view::extensions::ViewExtensions;
 // use zng_wgt_webrender_debug as wr;
diff --git a/examples/focus.rs b/examples/focus.rs
index 082d6003e..4a981ddc2 100644
--- a/examples/focus.rs
+++ b/examples/focus.rs
@@ -1,3 +1,5 @@
+//! Demonstrates the focus service, logical and directional navigation.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use zng::{
diff --git a/examples/gradient.rs b/examples/gradient.rs
index a0f25d7f2..34bf34fec 100644
--- a/examples/gradient.rs
+++ b/examples/gradient.rs
@@ -1,3 +1,5 @@
+//! Demonstrates gradient rendering.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use zng::{
diff --git a/examples/headless.rs b/examples/headless.rs
index f87c23f32..b8c80937e 100644
--- a/examples/headless.rs
+++ b/examples/headless.rs
@@ -1,3 +1,5 @@
+//! Demonstrates headless apps, image and video rendering.
+
 use std::{io::Write, path::PathBuf};
 
 use zng::{
diff --git a/examples/icon.rs b/examples/icon.rs
index b855f3de5..d9cb37600 100644
--- a/examples/icon.rs
+++ b/examples/icon.rs
@@ -1,3 +1,5 @@
+//! Search and copy Material Icons constants.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use zng::{
diff --git a/examples/image.rs b/examples/image.rs
index 5b0a42ae4..28429aa04 100644
--- a/examples/image.rs
+++ b/examples/image.rs
@@ -1,3 +1,5 @@
+//! Demonstrates image loading, displaying, animated sprites, rendering, pasting.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use std::path::PathBuf;
diff --git a/examples/layer.rs b/examples/layer.rs
index 74c450f87..87cd2359d 100644
--- a/examples/layer.rs
+++ b/examples/layer.rs
@@ -1,3 +1,5 @@
+//! Demonstrates the LAYERS service.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use zng::{
diff --git a/examples/localize.rs b/examples/localize.rs
index 3137ab082..9cfa4e2bf 100644
--- a/examples/localize.rs
+++ b/examples/localize.rs
@@ -1,3 +1,5 @@
+//! Demonstrates localization.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 use std::collections::HashMap;
 use std::path::PathBuf;
diff --git a/examples/markdown.rs b/examples/markdown.rs
index 98c0b6f5a..797c8f086 100644
--- a/examples/markdown.rs
+++ b/examples/markdown.rs
@@ -1,3 +1,5 @@
+//! Demonstrates the `Markdown!` widget.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 use std::path::PathBuf;
 
diff --git a/examples/res/screenshots/animation.png b/examples/res/screenshots/animation.png
new file mode 100644
index 000000000..0421fcab5
Binary files /dev/null and b/examples/res/screenshots/animation.png differ
diff --git a/examples/res/screenshots/border.png b/examples/res/screenshots/border.png
new file mode 100644
index 000000000..1df74aebe
Binary files /dev/null and b/examples/res/screenshots/border.png differ
diff --git a/examples/res/screenshots/button.png b/examples/res/screenshots/button.png
new file mode 100644
index 000000000..e379952b3
Binary files /dev/null and b/examples/res/screenshots/button.png differ
diff --git a/examples/res/screenshots/calculator.png b/examples/res/screenshots/calculator.png
new file mode 100644
index 000000000..00f0d1b6b
Binary files /dev/null and b/examples/res/screenshots/calculator.png differ
diff --git a/examples/res/screenshots/config.png b/examples/res/screenshots/config.png
new file mode 100644
index 000000000..c147f2570
Binary files /dev/null and b/examples/res/screenshots/config.png differ
diff --git a/examples/res/screenshots/cursor.png b/examples/res/screenshots/cursor.png
new file mode 100644
index 000000000..71cbd87a6
Binary files /dev/null and b/examples/res/screenshots/cursor.png differ
diff --git a/examples/res/screenshots/focus.png b/examples/res/screenshots/focus.png
new file mode 100644
index 000000000..6968bf409
Binary files /dev/null and b/examples/res/screenshots/focus.png differ
diff --git a/examples/res/screenshots/gradient.png b/examples/res/screenshots/gradient.png
new file mode 100644
index 000000000..6d874588a
Binary files /dev/null and b/examples/res/screenshots/gradient.png differ
diff --git a/examples/res/screenshots/headless.png b/examples/res/screenshots/headless.png
new file mode 100644
index 000000000..d77227507
Binary files /dev/null and b/examples/res/screenshots/headless.png differ
diff --git a/examples/res/screenshots/icon.png b/examples/res/screenshots/icon.png
new file mode 100644
index 000000000..26a29502c
Binary files /dev/null and b/examples/res/screenshots/icon.png differ
diff --git a/examples/res/screenshots/image.png b/examples/res/screenshots/image.png
new file mode 100644
index 000000000..a43f55efd
Binary files /dev/null and b/examples/res/screenshots/image.png differ
diff --git a/examples/res/screenshots/layer.png b/examples/res/screenshots/layer.png
new file mode 100644
index 000000000..2b26164cc
Binary files /dev/null and b/examples/res/screenshots/layer.png differ
diff --git a/examples/res/screenshots/localize.png b/examples/res/screenshots/localize.png
new file mode 100644
index 000000000..409de425b
Binary files /dev/null and b/examples/res/screenshots/localize.png differ
diff --git a/examples/res/screenshots/markdown.png b/examples/res/screenshots/markdown.png
new file mode 100644
index 000000000..258045e73
Binary files /dev/null and b/examples/res/screenshots/markdown.png differ
diff --git a/examples/res/screenshots/respawn.png b/examples/res/screenshots/respawn.png
new file mode 100644
index 000000000..9e25357fe
Binary files /dev/null and b/examples/res/screenshots/respawn.png differ
diff --git a/examples/res/screenshots/scroll.png b/examples/res/screenshots/scroll.png
new file mode 100644
index 000000000..38475a467
Binary files /dev/null and b/examples/res/screenshots/scroll.png differ
diff --git a/examples/res/screenshots/shortcut.png b/examples/res/screenshots/shortcut.png
new file mode 100644
index 000000000..c6b17d15f
Binary files /dev/null and b/examples/res/screenshots/shortcut.png differ
diff --git a/examples/res/screenshots/text.png b/examples/res/screenshots/text.png
new file mode 100644
index 000000000..19a6b54d8
Binary files /dev/null and b/examples/res/screenshots/text.png differ
diff --git a/examples/res/screenshots/transform.png b/examples/res/screenshots/transform.png
new file mode 100644
index 000000000..34ed32b62
Binary files /dev/null and b/examples/res/screenshots/transform.png differ
diff --git a/examples/res/screenshots/window.png b/examples/res/screenshots/window.png
new file mode 100644
index 000000000..47d2061d8
Binary files /dev/null and b/examples/res/screenshots/window.png differ
diff --git a/examples/respawn.rs b/examples/respawn.rs
index 601515542..d85247990 100644
--- a/examples/respawn.rs
+++ b/examples/respawn.rs
@@ -1,3 +1,5 @@
+//! Demonstrates the view-process respawn error recovery feature.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use zng::{
diff --git a/examples/scroll.rs b/examples/scroll.rs
index 78a9bd992..3263325ce 100644
--- a/examples/scroll.rs
+++ b/examples/scroll.rs
@@ -1,3 +1,5 @@
+//! Demonstrates the `Scroll!` widget and scroll commands.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use zng::{
diff --git a/examples/shortcuts.rs b/examples/shortcut.rs
similarity index 97%
rename from examples/shortcuts.rs
rename to examples/shortcut.rs
index 7be397f20..3f2e80ba0 100644
--- a/examples/shortcuts.rs
+++ b/examples/shortcut.rs
@@ -1,3 +1,5 @@
+//! Small utility that displays the pressed key gestures.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use zng::{font::FontName, layout::align, prelude::*};
@@ -61,7 +63,7 @@ fn app_main() {
             .perm();
 
         Window! {
-            title = "Shortcuts Example";
+            title = "Shortcut Example";
             auto_size = true;
             resizable = false;
             auto_size_origin = layout::Point::center();
diff --git a/examples/text.rs b/examples/text.rs
index 2a8435cdb..d8d2e3eaf 100644
--- a/examples/text.rs
+++ b/examples/text.rs
@@ -1,3 +1,5 @@
+//! Demonstrates the `Text!` and `TextInput!` widgets. Text rendering, text editor.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use core::fmt;
diff --git a/examples/transform.rs b/examples/transform.rs
index 87883195d..d0b2dde42 100644
--- a/examples/transform.rs
+++ b/examples/transform.rs
@@ -1,3 +1,5 @@
+//! Demonstrates 2D and 3D transforms, touch transforms.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use zng::{
diff --git a/examples/window.rs b/examples/window.rs
index fe4964fdd..93b3fb2f4 100644
--- a/examples/window.rs
+++ b/examples/window.rs
@@ -1,3 +1,5 @@
+//! Demonstrates the window widget, service, state and commands.
+
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use zng::{
@@ -629,7 +631,7 @@ fn confirm_close() -> impl WidgetHandler<WindowCloseRequestedArgs> {
     })
 }
 
-fn close_dialog(windows: Vec<WindowId>, state: ArcVar<CloseState>) -> impl UiNode {
+fn close_dialog(mut windows: Vec<WindowId>, state: ArcVar<CloseState>) -> impl UiNode {
     let opacity = var(0.fct());
     opacity.ease(1.fct(), 300.ms(), easing::linear).perm();
     Container! {
@@ -679,7 +681,8 @@ fn close_dialog(windows: Vec<WindowId>, state: ArcVar<CloseState>) -> impl UiNod
                                 child = Strong!("Close");
                                 on_click = hn_once!(state, |_| {
                                     state.set(CloseState::Close);
-                                    WINDOWS.close_together(windows).unwrap();
+                                    windows.retain(|w| WINDOWS.is_open(*w));
+                                    let _ = WINDOWS.close_together(windows);
                                 })
                             },
                             Button! {
diff --git a/tools/do-tasks/src/main.rs b/tools/do-tasks/src/main.rs
index 907ac94ad..51372761f 100644
--- a/tools/do-tasks/src/main.rs
+++ b/tools/do-tasks/src/main.rs
@@ -68,6 +68,7 @@ fn install(mut args: Vec<&str>) {
 // do doc [-o, --open] [<cargo-doc-args>]
 //        [-s, --serve]
 //        [--readme <crate>..]
+//        [--readme-examples <example>..]
 //
 //    Generate documentation for zng crates.
 //
@@ -85,12 +86,19 @@ fn install(mut args: Vec<&str>) {
 //         Tags:
 //            header: Replaces the next paragraph with the shared header.
 //            features: Replaces or insert the next section with the `## Cargo Features`.
+//     doc --readme-examples
+//         Update the examples/README.md file. Collects screenshots.
 fn doc(mut args: Vec<&str>) {
     if take_flag(&mut args, &["--readme"]) {
         readme_gen::generate(args);
         return;
     }
 
+    if take_flag(&mut args, &["--readme-examples"]) {
+        readme_gen::generate_examples(args);
+        return;
+    }
+
     let custom_open = if args.contains(&"--manifest-path") {
         if let Some(open) = args.iter_mut().find(|a| **a == "-o") {
             *open = "--open";
diff --git a/tools/do-tasks/src/readme_gen.rs b/tools/do-tasks/src/readme_gen.rs
index 076ed3431..b8cbee4ac 100644
--- a/tools/do-tasks/src/readme_gen.rs
+++ b/tools/do-tasks/src/readme_gen.rs
@@ -177,3 +177,82 @@ const README_TEMPLATE: &str = "\
 const FEATURES_HEADER: &str = "## Cargo Features";
 
 const SECTION_END: &str = "<!--do doc --readme #SECTION-END-->";
+
+/*
+*  EXAMPLES README
+*/
+
+pub fn generate_examples(_args: Vec<&str>) {
+    use std::fmt::*;
+
+    let cargo = std::fs::read_to_string("examples/Cargo.toml").unwrap();
+    let mut is_example = false;
+
+    let mut examples = vec![];
+
+    let mut name = String::new();
+    for line in cargo.lines() {
+        let line = line.trim();
+
+        if line == "[[example]]" {
+            is_example = true;
+        } else if line.starts_with('[') {
+            is_example = false;
+        } else if is_example {
+            if let Some(n) = line.strip_prefix("name = ") {
+                name = n.trim_matches(&['"', ' ']).to_owned();
+            } else if let Some(p) = line.strip_prefix("path = ") {
+                let p = p.trim_matches(&['"', ' ']);
+                if p != format!("{name}.rs") {
+                    crate::error(format_args!("expected example `{name}` to have the same file name, but was `{p}`"));
+                } else {
+                    examples.push(std::mem::take(&mut name));
+                }
+            }
+        }
+    }
+
+    let mut readme = std::fs::read_to_string("").unwrap_or_default();
+
+    const TAG: &str = "<!--do doc --readme-examples-->";
+    if let Some(i) = readme.find(TAG) {
+        readme.truncate(i + TAG.len());
+        writeln!(&mut readme).unwrap();
+    } else {
+        writeln!(&mut readme, "\n{TAG}").unwrap();
+    }
+
+    for example in examples {
+        let file = format!("examples/{example}.rs");
+        println(&file);
+
+        let file = std::fs::read_to_string(file).unwrap();
+
+        let mut docs = String::new();
+        for line in file.lines() {
+            let line = line.trim();
+
+            if let Some(doc) = line.strip_prefix("//!") {
+                writeln!(&mut docs, "{}", doc.trim_start()).unwrap();
+            }
+        }
+
+        writeln!(&mut readme, "### `{example}`\n").unwrap();
+
+        let screenshot = format!("./res/screenshots/{example}.png");
+        if PathBuf::from("examples").join(&screenshot).exists() {
+            writeln!(&mut readme, "<img alt='headless screenshot' src='{screenshot}' width='300'>\n",).unwrap();
+        }
+
+        writeln!(&mut readme, "Source: [{example}.rs](./{example}.rs)\n").unwrap();
+        writeln!(&mut readme, "```console\ncargo do run {example}\n```\n").unwrap();
+
+        if docs.is_empty() {
+            crate::error(format_args!("missing docs"));
+        } else {
+            writeln!(&mut readme, "{docs}").unwrap();
+        }
+    }
+
+    std::fs::write("examples/README.md", readme).unwrap();
+}
diff --git a/zng-ext-window/src/service.rs b/zng-ext-window/src/service.rs
index 2ed93d0ff..fe6f96446 100644
--- a/zng-ext-window/src/service.rs
+++ b/zng-ext-window/src/service.rs
@@ -9,8 +9,8 @@ use zng_app::{
     view_process::{
         self,
         raw_events::{
-            RAW_COLOR_SCHEME_CHANGED_EVENT, RAW_WINDOW_CLOSE_EVENT, RAW_WINDOW_CLOSE_REQUESTED_EVENT, RAW_WINDOW_FOCUS_EVENT,
-            RAW_WINDOW_OPEN_EVENT,
+            RAW_COLOR_SCHEME_CHANGED_EVENT, RAW_IMAGE_LOADED_EVENT, RAW_IMAGE_LOAD_ERROR_EVENT, RAW_WINDOW_CLOSE_EVENT,
+            RAW_WINDOW_CLOSE_REQUESTED_EVENT, RAW_WINDOW_FOCUS_EVENT, RAW_WINDOW_OPEN_EVENT,
         },
         ViewImage, ViewRenderer, VIEW_PROCESS, VIEW_PROCESS_INITED_EVENT,
     },
@@ -32,7 +32,10 @@ use zng_task::{
 };
 use zng_txt::{formatx, Txt};
 use zng_unique_id::{IdMap, IdSet};
-use zng_var::{impl_from_and_into_var, response_done_var, response_var, var, ArcVar, BoxedVar, LocalVar, ResponderVar, ResponseVar, Var};
+use zng_var::{
+    impl_from_and_into_var, response_done_var, response_var, types::WeakArcVar, var, AnyWeakVar, ArcVar, BoxedVar, LocalVar, ResponderVar,
+    ResponseVar, Var, WeakVar,
+};
 use zng_view_api::{
     api_extension::{ApiExtensionId, ApiExtensionPayload},
     config::ColorScheme,
@@ -70,7 +73,7 @@ pub(super) struct WindowsService {
     focus_request: Option<WindowId>,
     bring_to_top_requests: Vec<WindowId>,
 
-    frame_images: Vec<ArcVar<Img>>,
+    frame_images: Vec<WeakArcVar<Img>>,
 
     loading_deadline: Option<DeadlineHandle>,
     latest_color_scheme: ColorScheme,
@@ -166,7 +169,8 @@ impl WindowsService {
                     Ok(img) => {
                         let img = Img::new(img);
                         let img = var(img);
-                        self.frame_images.push(img.clone());
+                        self.frame_images.retain(|i| i.strong_count() > 0);
+                        self.frame_images.push(img.downgrade());
                         img.read_only()
                     }
                     Err(_) => var(Img::dummy(Some(formatx!("{}", WindowNotFound(window_id))))).read_only(),
@@ -802,6 +806,21 @@ impl WINDOWS {
 
             // we skipped request fulfillment until this event.
             UPDATES.update(None);
+        } else if let Some(args) = RAW_IMAGE_LOADED_EVENT.on(update).or_else(|| RAW_IMAGE_LOAD_ERROR_EVENT.on(update)) {
+            // update ready frame images.
+            let mut sv = WINDOWS_SV.write();
+            sv.frame_images.retain(|i| {
+                if let Some(i) = i.upgrade() {
+                    if Some(&args.image) == i.get().view() {
+                        i.update();
+                        false
+                    } else {
+                        true
+                    }
+                } else {
+                    false
+                }
+            });
         }
 
         Self::with_detached_windows(|windows, parallel| {
diff --git a/zng-task/src/ui.rs b/zng-task/src/ui.rs
index d8e1b509c..6c2c4d86b 100644
--- a/zng-task/src/ui.rs
+++ b/zng-task/src/ui.rs
@@ -3,6 +3,7 @@
 use std::{
     fmt,
     future::Future,
+    mem,
     pin::Pin,
     task::{Poll, Waker},
 };
@@ -15,12 +16,14 @@ enum UiTaskState<R> {
         last_update: Option<zng_var::VarUpdateId>,
     },
     Ready(R),
+    Cancelled,
 }
 impl<R: fmt::Debug> fmt::Debug for UiTaskState<R> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
             Self::Pending { .. } => write!(f, "Pending"),
             Self::Ready(arg0) => f.debug_tuple("Ready").field(arg0).finish(),
+            Self::Cancelled => unreachable!(),
         }
     }
 }
@@ -106,10 +109,30 @@ impl<R> UiTask<R> {
     /// then call this method to take ownership of the result.
     ///
     /// [`update`]: Self::update
-    pub fn into_result(self) -> Result<R, Self> {
-        match self.0 {
+    pub fn into_result(mut self) -> Result<R, Self> {
+        match mem::replace(&mut self.0, UiTaskState::Cancelled) {
             UiTaskState::Ready(r) => Ok(r),
             p @ UiTaskState::Pending { .. } => Err(Self(p)),
+            UiTaskState::Cancelled => unreachable!(),
+        }
+    }
+
+    /// Drop the task without logging a warning if it is pending.
+    pub fn cancel(mut self) {
+        self.0 = UiTaskState::Cancelled;
+    }
+}
+impl<R> Drop for UiTask<R> {
+    fn drop(&mut self) {
+        if let UiTaskState::Pending { .. } = &self.0 {
+            #[cfg(debug_assertions)]
+            {
+                tracing::warn!("pending UiTask<{}> dropped", std::any::type_name::<R>());
+            }
+            #[cfg(not(debug_assertions))]
+            {
+                tracing::warn!("pending UiTask dropped");
+            }
         }
     }
 }
diff --git a/zng-txt/src/lib.rs b/zng-txt/src/lib.rs
index 56b9740d8..9fd63e06a 100644
--- a/zng-txt/src/lib.rs
+++ b/zng-txt/src/lib.rs
@@ -660,6 +660,9 @@ impl<T: ToString> ToTxt for T {
 #[macro_export]
 macro_rules! formatx {
     ($($tt:tt)*) => {
-        $crate::Txt::from_fmt(format_args!($($tt)*))
+        {
+            let res = $crate::Txt::from_fmt(format_args!($($tt)*));
+            res
+        }
     };
 }
diff --git a/zng-var/src/response.rs b/zng-var/src/response.rs
index 96f6edc40..c8af40dd2 100644
--- a/zng-var/src/response.rs
+++ b/zng-var/src/response.rs
@@ -128,7 +128,7 @@ impl<T: VarValue> ResponseVar<T> {
     ///
     /// [`rsp`]: Self::rsp
     pub async fn wait_done(&self) {
-        self.wait_value(Response::is_done).await
+        self.wait_value(Response::is_done).await;
     }
 
     /// Clone the response, if present and new.
diff --git a/zng-view/src/image_cache.rs b/zng-view/src/image_cache.rs
index 077db7fa4..1aab473df 100644
--- a/zng-view/src/image_cache.rs
+++ b/zng-view/src/image_cache.rs
@@ -1222,7 +1222,9 @@ mod capture {
     use super::ImageCache;
 
     impl ImageCache {
-        /// Create frame_image for a `Api::frame_image` request.
+        /// Create frame_image for an `Api::frame_image` request.
+        ///
+        /// Gl context must be current.
         #[allow(clippy::too_many_arguments)]
         pub fn frame_image(
             &mut self,
@@ -1265,6 +1267,8 @@ mod capture {
         }
 
         /// Create frame_image for a capture request in the FrameRequest.
+        ///
+        /// Gl context must be current.
         pub fn frame_image_data(
             &mut self,
             renderer: &mut Renderer,
@@ -1299,7 +1303,7 @@ mod capture {
             data
         }
 
-        pub fn frame_image_data_impl(
+        fn frame_image_data_impl(
             &mut self,
             renderer: &mut Renderer,
             rect: PxRect,
diff --git a/zng-view/src/surface.rs b/zng-view/src/surface.rs
index 5f598ab25..6946c2450 100644
--- a/zng-view/src/surface.rs
+++ b/zng-view/src/surface.rs
@@ -404,6 +404,7 @@ impl Surface {
                 FrameCapture::Mask(m) => Some(Some(m)),
             };
             if let Some(mask) = capture {
+                self.context.make_current();
                 captured_data = Some(images.frame_image_data(
                     renderer,
                     PxRect::from_size(self.size.to_px(self.scale_factor)),
@@ -417,6 +418,7 @@ impl Surface {
     }
 
     pub fn frame_image(&mut self, images: &mut ImageCache, mask: Option<ImageMaskMode>) -> ImageId {
+        self.context.make_current();
         images.frame_image(
             self.renderer.as_mut().unwrap(),
             PxRect::from_size(self.size.to_px(self.scale_factor)),
@@ -430,6 +432,7 @@ impl Surface {
 
     pub fn frame_image_rect(&mut self, images: &mut ImageCache, rect: PxRect, mask: Option<ImageMaskMode>) -> ImageId {
         let rect = PxRect::from_size(self.size.to_px(self.scale_factor)).intersection(&rect).unwrap();
+        self.context.make_current();
         images.frame_image(
             self.renderer.as_mut().unwrap(),
             rect,
diff --git a/zng-view/src/window.rs b/zng-view/src/window.rs
index 0f664f433..8b793a326 100644
--- a/zng-view/src/window.rs
+++ b/zng-view/src/window.rs
@@ -1396,6 +1396,7 @@ impl Window {
                 self.redraw();
             }
             let renderer = self.renderer.as_mut().unwrap();
+            self.context.make_current();
             Some(images.frame_image_data(
                 renderer,
                 PxRect::from_size(self.window.inner_size().to_px()),
@@ -1459,6 +1460,7 @@ impl Window {
 
     pub fn frame_image(&mut self, images: &mut ImageCache, mask: Option<ImageMaskMode>) -> ImageId {
         let scale_factor = self.scale_factor();
+        self.context.make_current();
         images.frame_image(
             self.renderer.as_mut().unwrap(),
             PxRect::from_size(self.window.inner_size().to_px()),
@@ -1475,6 +1477,7 @@ impl Window {
         let rect = PxRect::from_size(self.window.inner_size().to_px())
             .intersection(&rect)
             .unwrap_or_default();
+        self.context.make_current();
         images.frame_image(
             self.renderer.as_mut().unwrap(),
             rect,
diff --git a/zng-wgt-inspector/Cargo.toml b/zng-wgt-inspector/Cargo.toml
index cc2cce290..7328fb159 100644
--- a/zng-wgt-inspector/Cargo.toml
+++ b/zng-wgt-inspector/Cargo.toml
@@ -23,6 +23,8 @@ zng-wgt = { path = "../zng-wgt", version = "0.2.7" }
 zng-app = { path = "../zng-app", version = "0.3.3" }
 zng-ext-input = { path = "../zng-ext-input", version = "0.3.3" }
 zng-ext-font = { path = "../zng-ext-font", version = "0.3.3" }
+zng-ext-clipboard = { path = "../zng-ext-clipboard", version = "0.2.7" }
+zng-ext-image = { path = "../zng-ext-image", version = "0.2.7" }
 zng-ext-config = { path = "../zng-ext-config", version = "0.2.7", default-features = false }
 zng-ext-l10n = { path = "../zng-ext-l10n", version = "0.3.3" }
 zng-view-api = { path = "../zng-view-api", version = "0.2.7" }
@@ -38,6 +40,7 @@ zng-wgt-text-input = { path = "../zng-wgt-text-input", version = "0.2.8" }
 zng-wgt-container = { path = "../zng-wgt-container", version = "0.2.7" }
 zng-wgt-input = { path = "../zng-wgt-input", version = "0.2.7" }
 zng-wgt-scroll = { path = "../zng-wgt-scroll", version = "0.2.7" }
+zng-wgt-menu = { path = "../zng-wgt-menu", version = "0.2.8" }
 zng-wgt-stack = { path = "../zng-wgt-stack", version = "0.2.7" }
 zng-wgt-size-offset = { path = "../zng-wgt-size-offset", version = "0.2.7" }
 zng-wgt-wrap = { path = "../zng-wgt-wrap", version = "0.2.8" }
diff --git a/zng-wgt-inspector/src/live.rs b/zng-wgt-inspector/src/live.rs
index e13173b54..56c4df892 100644
--- a/zng-wgt-inspector/src/live.rs
+++ b/zng-wgt-inspector/src/live.rs
@@ -282,6 +282,7 @@ mod inspector_window {
     use zng_var::{animation::easing, var_from};
     use zng_wgt::{border, corner_radius, margin, prelude::*, visibility, Wgt};
     use zng_wgt_access::{access_role, AccessRole};
+    use zng_wgt_button::Button;
     use zng_wgt_container::{child_align, padding, Container};
     use zng_wgt_fill::{background, background_color};
     use zng_wgt_filter::opacity;
@@ -470,8 +471,53 @@ mod inspector_window {
                         };
                         tooltip = Tip!(Text!("select focused widget"));
                         checked = select_focused;
+                    },
+                    zng_wgt_rule_line::vr::Vr!(),
+                    Toggle! {
+                        child = Stack! {
+                            size = (14, 10);
+                            direction = StackDirection::top_to_bottom();
+                            zng_wgt_rule_line::hr::margin = 0;
+                            zng_wgt_rule_line::hr::color = zng_wgt_text::FONT_COLOR_VAR;
+                            spacing = 3;
+                            children = ui_vec![
+                                zng_wgt_rule_line::hr::Hr!(),
+                                zng_wgt_rule_line::hr::Hr!(),
+                                zng_wgt_rule_line::hr::Hr!(),
+                            ]
+                        };
+                        checked = var(false);
+                        checked_popup = {
+                            let screenshot_idle = var(true);
+                            wgt_fn!(screenshot_idle, |_| {
+                                zng_wgt_menu::context::ContextMenu!(ui_vec![
+                                    Button! {
+                                        child = Text!("Save Screenshot");
+                                        zng_wgt::enabled = screenshot_idle.clone();
+                                        on_click = hn!(screenshot_idle, |_| {
+                                            // not async_hn here because menu is dropped on click
+                                            task::spawn(async_clmv!(screenshot_idle, {
+                                                screenshot_idle.set(false);
+                                                save_screenshot(inspected().unwrap(), WINDOW.id()).await;
+                                                screenshot_idle.set(true);
+                                            }));
+                                        });
+                                    },
+                                    Button! {
+                                        child = Text!("Copy Screenshot");
+                                        zng_wgt::enabled = screenshot_idle.clone();
+                                        on_click = hn!(screenshot_idle, |_| {
+                                            task::spawn(async_clmv!(screenshot_idle, {
+                                                screenshot_idle.set(false);
+                                                copy_screenshot(inspected().unwrap(), WINDOW.id()).await;
+                                                screenshot_idle.set(true);
+                                            }));
+                                        });
+                                    },
+                                ])
+                            })
+                        };
                     }
-
                 ]
             }, 0;
             child = TextInput! {
@@ -910,6 +956,73 @@ mod inspector_window {
 
         false
     }
+
+    async fn save_screenshot(inspected: WindowId, inspector: WindowId) {
+        let frame = WINDOWS.frame_image(inspected, None);
+
+        let mut dlg = zng_view_api::dialog::FileDialog {
+            title: "Save Screenshot".into(),
+            kind: zng_view_api::dialog::FileDialogKind::SaveFile,
+            starting_name: "screenshot.png".into(),
+            ..Default::default()
+        };
+        let encoders = zng_ext_image::IMAGES.available_encoders();
+        for enc in &encoders {
+            dlg.push_filter(&enc.to_uppercase(), &[enc]);
+        }
+        dlg.push_filter("Image Files", &encoders);
+
+        let r = WINDOWS.native_file_dialog(inspector, dlg).wait_rsp().await;
+        let path = match r {
+            zng_view_api::dialog::FileDialogResponse::Selected(mut p) => p.remove(0),
+            zng_view_api::dialog::FileDialogResponse::Cancel => return,
+            zng_view_api::dialog::FileDialogResponse::Error(e) => {
+                screenshot_error(e, inspector).await;
+                return;
+            }
+        };
+
+        frame.wait_value(|f| !f.is_loading()).await;
+        let frame = frame.get();
+
+        if let Some(e) = frame.error() {
+            screenshot_error(e, inspector).await;
+        } else {
+            let r = frame.save(path).await;
+            if let Err(e) = r {
+                screenshot_error(formatx!("Screenshot save error. {e}"), inspector).await;
+            }
+        }
+    }
+
+    async fn copy_screenshot(inspected: WindowId, inspector: WindowId) {
+        let frame = WINDOWS.frame_image(inspected, None);
+
+        frame.wait_value(|f| !f.is_loading()).await;
+        let frame = frame.get();
+
+        if let Some(e) = frame.error() {
+            screenshot_error(e, inspector).await;
+        } else {
+            let r = zng_ext_clipboard::CLIPBOARD.set_image(frame).wait_rsp().await;
+            if let Err(e) = r {
+                screenshot_error(formatx!("Clipboard error. {e}"), inspector).await;
+            }
+        }
+    }
+
+    async fn screenshot_error(e: Txt, inspector: WindowId) {
+        let r = WINDOWS.native_message_dialog(
+            inspector,
+            zng_view_api::dialog::MsgDialog {
+                title: "Screenshot Error".into(),
+                message: formatx!("Screenshot error.\n\n{e}"),
+                icon: zng_view_api::dialog::MsgDialogIcon::Error,
+                ..Default::default()
+            },
+        );
+        r.wait_done().await;
+    }
 }
 
 mod data_model {
diff --git a/zng-wgt-toggle/src/lib.rs b/zng-wgt-toggle/src/lib.rs
index 81f74576c..f27689fb6 100644
--- a/zng-wgt-toggle/src/lib.rs
+++ b/zng-wgt-toggle/src/lib.rs
@@ -1081,6 +1081,9 @@ pub fn combo_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl
 ///
 /// The `popup` can be any widget, that will be open using [`POPUP`], a [`Popup!`] or derived widget is recommended.
 ///
+/// Note that if the checked property is not set the toggle will never be checked, to implement a drop-down menu
+/// set `checked = var(false);`.
+///
 /// [`ComboStyle!`]: struct@ComboStyle
 /// [`Popup!`]: struct@zng_wgt_layer::popup::Popup
 /// [`LAYERS`]: layers::LAYERS


Test patch: