#1244: navigator and layer thumbnails render off the UI thread and wait for drags to end
The navigator re-rendered the whole document on the UI thread on every revision, including each drag step, and layer thumbnails re-rendered whole layers the same way. Both now keep the last image while an interaction runs, render on a background worker once the document settles (synchronously on wasm and in tests), and turn on image mips. Thumbnails are keyed by node id with a Weak, not by a reusable address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
This commit is contained in:
parent
bbbafdef6b
commit
9139883075
|
|
@ -305,6 +305,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
smooth_images,
|
||||
// Isolation mode: the art around the isolated group or layer is dimmed (#833).
|
||||
isolated: key.isolated.map(vectorcraft_doc::NodeId),
|
||||
// Images far smaller on screen than they are sample their mip levels (#1244).
|
||||
image_mips: true,
|
||||
..opts
|
||||
};
|
||||
// Light documents render synchronously (no lag vs overlays); heavy ones go to the worker.
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ pub mod shortcuts;
|
|||
pub mod state;
|
||||
pub mod sysclip;
|
||||
pub mod theme;
|
||||
pub mod thumb_worker;
|
||||
pub mod titlebar;
|
||||
pub mod toolbar;
|
||||
mod touch;
|
||||
|
|
|
|||
|
|
@ -68,10 +68,10 @@ pub(crate) fn paint_thumb(ui: &Ui, st: &DocState, a: &ExportAsset, r: Rect) {
|
|||
let tex = CACHE.with(|c| c.borrow().get(&key).cloned()).or_else(|| {
|
||||
let art: Vec<&Node> = ids().iter().filter_map(|id| st.doc.node(*id)).collect();
|
||||
let img = RENDERER.with(|rr| match art.as_slice() {
|
||||
[one] => rr.borrow_mut().render_node_thumbnail(&st.doc, one, px, None),
|
||||
[one] => rr.borrow_mut().render_node_thumbnail_with(&st.doc, one, px, None, &super::layers::thumb_options()),
|
||||
many => {
|
||||
let group = Node::group(NodeId(0), many.iter().map(|n| Arc::new((*n).clone())).collect());
|
||||
rr.borrow_mut().render_node_thumbnail(&st.doc, &group, px, None)
|
||||
rr.borrow_mut().render_node_thumbnail_with(&st.doc, &group, px, None, &super::layers::thumb_options())
|
||||
}
|
||||
})?;
|
||||
let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@
|
|||
//! those scrolled into view are laid out, so a document of many thousand objects costs no more
|
||||
//! per frame than a small one.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use egui::{Color32, Sense, Stroke, StrokeKind, Ui, vec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -40,6 +40,7 @@ use vectorcraft_doc::{Document, Node, NodeId, NodeKind};
|
|||
use vectorcraft_engine::OpenRows;
|
||||
|
||||
use crate::theme::Tokens;
|
||||
use crate::thumb_worker::{Settle, ThumbWorker};
|
||||
use crate::widgets::{Live, PanelDrag};
|
||||
use crate::{VectorcraftApp, icons, widgets};
|
||||
|
||||
|
|
@ -136,12 +137,12 @@ fn colour_bar(ui: &Ui, n: &Node, r: egui::Rect, x: f32, colour: Color32) {
|
|||
}
|
||||
|
||||
/// The thumbnail of row `n` (rect `r`) at `x`; returns where it went.
|
||||
fn row_thumb(ui: &Ui, view: &View, n: &Node, r: egui::Rect, x: f32) -> egui::Rect {
|
||||
fn row_thumb(ui: &Ui, view: &View, n: &Arc<Node>, r: egui::Rect, x: f32) -> egui::Rect {
|
||||
let s = (view.h - 4.0).min(40.0);
|
||||
let th = egui::Rect::from_min_size(egui::pos2(x, r.center().y - s / 2.0), vec2(s, s));
|
||||
ui.painter().rect_filled(th, 0.0, Color32::WHITE);
|
||||
ui.painter().rect_stroke(th, 0.0, Stroke::new(1.0, view.t.border), StrokeKind::Outside);
|
||||
if !real_thumb(ui, view.doc, n, th) {
|
||||
if !real_thumb(ui, &view.thumbs, n, th) {
|
||||
thumb(ui, n, th);
|
||||
}
|
||||
th
|
||||
|
|
@ -151,7 +152,8 @@ fn row_thumb(ui: &Ui, view: &View, n: &Node, r: egui::Rect, x: f32) -> egui::Rec
|
|||
/// colour bar, thumbnail and name.
|
||||
fn drag_ghost(ui: &Ui, view: &View, src: NodeId, pos: egui::Pos2) {
|
||||
let (doc, t, h) = (view.doc, &view.t, view.h);
|
||||
let Some(n) = doc.node(src) else { return };
|
||||
let Some(n) = node_arc(doc, src) else { return };
|
||||
let n = &n;
|
||||
let (grab, width, depth) = ui.data(|d| d.get_temp::<(egui::Vec2, f32, usize)>(key("grab"))).unwrap_or((vec2(h, h / 2.0), 240.0, 0));
|
||||
// A plain Ui on the tooltip layer, not an Area: a new Area is invisible for a frame and fades in.
|
||||
let r = egui::Rect::from_min_size(pos - grab, vec2(width, h));
|
||||
|
|
@ -312,7 +314,9 @@ fn rows_of_doc(ui: &Ui, st: &vectorcraft_engine::DocState, opts: &PanelOptions,
|
|||
|
||||
/// Everything one frame of the panel reads.
|
||||
struct View<'a> {
|
||||
doc: &'a Document,
|
||||
doc: &'a Arc<Document>,
|
||||
/// Where the row thumbnails come from.
|
||||
thumbs: ThumbSource<'a>,
|
||||
sel: HashSet<NodeId>,
|
||||
/// The selected ruler guides.
|
||||
guides: Vec<usize>,
|
||||
|
|
@ -375,6 +379,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
let list = rows_of_doc(ui, st, &app.ui.layers_panel, &query);
|
||||
let view = View {
|
||||
doc: &doc,
|
||||
thumbs: ThumbSource::of(app, st, &doc),
|
||||
sel: st.selection.objects.iter().copied().collect(),
|
||||
guides: st.selection.guides.clone(),
|
||||
target: st.selection.target,
|
||||
|
|
@ -676,7 +681,7 @@ fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
|
|||
x += 16.0;
|
||||
// Thumbnail.
|
||||
let thumb_rect = view.opts.thumb(n).then(|| {
|
||||
let th = row_thumb(ui, view, n, r, x);
|
||||
let th = row_thumb(ui, view, &item.node, r, x);
|
||||
x = th.right() + 4.0;
|
||||
th
|
||||
});
|
||||
|
|
@ -941,35 +946,215 @@ fn has_styled_target(n: &Node) -> bool {
|
|||
|| !n.has_default_transparency()
|
||||
}
|
||||
|
||||
/// A real rendered thumbnail, cached by node identity (unchanged nodes keep their `Arc`
|
||||
/// allocation, so the address is a free change detector). Only rendered for visible rows.
|
||||
pub(crate) fn real_thumb(ui: &Ui, doc: &vectorcraft_doc::Document, n: &Node, r: egui::Rect) -> bool {
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
thread_local! {
|
||||
static RENDERER: RefCell<vectorcraft_render::Renderer> = RefCell::new(vectorcraft_render::Renderer::new());
|
||||
static CACHE: crate::graphics::TexCache<HashMap<(usize, u64), egui::TextureHandle>> = crate::graphics::TexCache::default();
|
||||
/// Where row thumbnails come from: document `uid` at `revision`, and whether a drag is in
|
||||
/// progress (the thumbnails shown then stay until it ends).
|
||||
pub(crate) struct ThumbSource<'a> {
|
||||
pub uid: u64,
|
||||
pub revision: u64,
|
||||
pub doc: &'a Arc<Document>,
|
||||
pub busy: bool,
|
||||
}
|
||||
|
||||
impl<'a> ThumbSource<'a> {
|
||||
/// The thumbnails of the document `st` holds (`doc`, its document).
|
||||
pub(crate) fn of(app: &VectorcraftApp, st: &vectorcraft_engine::DocState, doc: &'a Arc<Document>) -> Self {
|
||||
Self { uid: st.uid, revision: st.revision, doc, busy: st.interaction.is_some() || app.session.tool_busy() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Node `id` of `doc`, as the `Arc` its parent holds.
|
||||
pub(crate) fn node_arc(doc: &Document, id: NodeId) -> Option<Arc<Node>> {
|
||||
let path = doc.index_path(id)?;
|
||||
let (first, rest) = path.split_first()?;
|
||||
let mut cur = doc.layers.get(*first)?;
|
||||
for &i in rest {
|
||||
cur = cur.children()?.get(i)?;
|
||||
}
|
||||
Some(cur.clone())
|
||||
}
|
||||
|
||||
/// A row thumbnail's image.
|
||||
struct ThumbEntry {
|
||||
/// The node it shows. Weak, so the cache doesn't keep old art alive, but its allocation stays
|
||||
/// and no other node can come to have its address.
|
||||
node: Weak<Node>,
|
||||
px: u32,
|
||||
/// None: the node draws nothing.
|
||||
tex: Option<egui::TextureHandle>,
|
||||
/// How long it took to render (ms).
|
||||
ms: f64,
|
||||
/// The pass it was last drawn in.
|
||||
used: u64,
|
||||
}
|
||||
|
||||
/// A row thumbnail job: the row (document uid, node), the node and the size rendered.
|
||||
type ThumbJob = ((u64, NodeId), Weak<Node>, u32);
|
||||
|
||||
#[derive(Default)]
|
||||
struct Thumbs {
|
||||
entries: HashMap<(u64, NodeId), ThumbEntry>,
|
||||
/// What the worker is rendering, by row.
|
||||
pending: HashMap<(u64, NodeId), (Weak<Node>, u32)>,
|
||||
worker: Option<ThumbWorker<(u64, NodeId), ThumbJob>>,
|
||||
worker_started: bool,
|
||||
settle: Settle,
|
||||
/// (pass, ms, count) rendered on the UI thread in that pass.
|
||||
spent: (u64, f64, u32),
|
||||
/// Thumbnails rendered on the UI thread (tests count them).
|
||||
renders: u64,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static THUMBS: crate::graphics::TexCache<Thumbs> = crate::graphics::TexCache::default();
|
||||
}
|
||||
|
||||
/// The most row thumbnails kept: past it the ones drawn longest ago go.
|
||||
const MAX_THUMBS: usize = 512;
|
||||
/// Rendering a thumbnail that took longer than this (ms) again goes to the worker.
|
||||
const LIGHT_THUMB_MS: f64 = 4.0;
|
||||
/// A node with more objects than this inside has its first thumbnail made on the worker.
|
||||
const LIGHT_THUMB_NODES: usize = 200;
|
||||
/// At most this long (ms) and this many thumbnails are rendered on the UI thread in a pass; the
|
||||
/// rest go to the worker, or wait for the next pass where there is none.
|
||||
const THUMB_BUDGET_MS: f64 = 8.0;
|
||||
const THUMB_BUDGET_COUNT: u32 = 16;
|
||||
|
||||
/// How row thumbnails render: images far smaller than they are sample their mip levels.
|
||||
pub(crate) fn thumb_options() -> vectorcraft_render::RenderOptions {
|
||||
vectorcraft_render::RenderOptions { image_mips: true, ..Default::default() }
|
||||
}
|
||||
|
||||
/// Has `n` at most `limit` objects inside it (counting stops there)?
|
||||
fn small_node(n: &Node, limit: usize) -> bool {
|
||||
fn count(n: &Node, left: &mut usize) -> bool {
|
||||
for c in n.children().into_iter().flatten() {
|
||||
if *left == 0 {
|
||||
return false;
|
||||
}
|
||||
*left -= 1;
|
||||
if !count(c, left) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
let mut left = limit;
|
||||
count(n, &mut left)
|
||||
}
|
||||
|
||||
impl Thumbs {
|
||||
/// Keep `img` (None: nothing drawn) as the thumbnail of row `key` showing `node`.
|
||||
fn upload(&mut self, ctx: &egui::Context, key: (u64, NodeId), node: Weak<Node>, px: u32, img: Option<&vectorcraft_render::Rendered>, ms: f64) {
|
||||
let color = img.map(|img| egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels));
|
||||
let used = ctx.cumulative_pass_nr();
|
||||
let name = format!("layer-thumb-{}", key.1.0);
|
||||
match self.entries.get_mut(&key) {
|
||||
Some(e) => {
|
||||
match (color, &mut e.tex) {
|
||||
(Some(color), Some(tex)) => tex.set(color, egui::TextureOptions::LINEAR),
|
||||
(Some(color), tex) => *tex = Some(ctx.load_texture(name, color, egui::TextureOptions::LINEAR)),
|
||||
(None, tex) => *tex = None,
|
||||
}
|
||||
(e.node, e.px, e.ms, e.used) = (node, px, ms, used);
|
||||
}
|
||||
None => {
|
||||
let tex = color.map(|color| ctx.load_texture(name, color, egui::TextureOptions::LINEAR));
|
||||
self.entries.insert(key, ThumbEntry { node, px, tex, ms, used });
|
||||
if self.entries.len() > MAX_THUMBS {
|
||||
// Keep the three quarters drawn last.
|
||||
let mut by_use: Vec<u64> = self.entries.values().map(|e| e.used).collect();
|
||||
by_use.sort_unstable();
|
||||
let cut = by_use.get(by_use.len() - MAX_THUMBS * 3 / 4).copied().unwrap_or(0);
|
||||
self.entries.retain(|_, e| e.used >= cut);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The texture of row `n` (`px` pixels square) to draw: made or updated now, or on the
|
||||
/// worker (the old one is drawn meanwhile). Thumbnails shown stay during a drag and until
|
||||
/// the document settles.
|
||||
fn texture(&mut self, ctx: &egui::Context, src: &ThumbSource, n: &Arc<Node>, px: u32) -> Option<egui::TextureHandle> {
|
||||
if !self.worker_started {
|
||||
self.worker_started = true;
|
||||
self.worker = ThumbWorker::spawn(ctx, "thumbnails");
|
||||
}
|
||||
let done = match self.worker.as_ref().map(ThumbWorker::poll) {
|
||||
Some(Some(done)) => done,
|
||||
Some(None) => {
|
||||
// The thread is gone: render here from now on.
|
||||
self.worker = None;
|
||||
self.pending.clear();
|
||||
vec![]
|
||||
}
|
||||
None => vec![],
|
||||
};
|
||||
for d in done {
|
||||
let (key, node, px) = d.key;
|
||||
if self.pending.get(&key).is_some_and(|(p, ppx)| p.ptr_eq(&node) && *ppx == px) {
|
||||
self.pending.remove(&key);
|
||||
}
|
||||
self.upload(ctx, key, node, px, d.img.as_ref(), d.ms);
|
||||
}
|
||||
let key = (src.uid, n.id);
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
let (fresh, light) = match self.entries.get_mut(&key) {
|
||||
Some(e) => {
|
||||
e.used = pass;
|
||||
(std::ptr::eq(e.node.as_ptr(), Arc::as_ptr(n)) && e.px == px, Some(e.ms <= LIGHT_THUMB_MS))
|
||||
}
|
||||
None => (false, None),
|
||||
};
|
||||
let shown = self.entries.get(&key).and_then(|e| e.tex.clone());
|
||||
if fresh || self.pending.get(&key).is_some_and(|(p, ppx)| std::ptr::eq(p.as_ptr(), Arc::as_ptr(n)) && *ppx == px) {
|
||||
return shown;
|
||||
}
|
||||
if light.is_some() && (src.busy || !self.settle.settled(ctx, src.uid, src.revision, ctx.input(|i| i.time))) {
|
||||
return shown;
|
||||
}
|
||||
if self.spent.0 != pass {
|
||||
self.spent = (pass, 0.0, 0);
|
||||
}
|
||||
let in_budget = self.spent.1 < THUMB_BUDGET_MS && self.spent.2 < THUMB_BUDGET_COUNT;
|
||||
let light = light.unwrap_or_else(|| small_node(n, LIGHT_THUMB_NODES)) && in_budget;
|
||||
if !light && let Some(w) = &self.worker {
|
||||
let (doc, node) = (src.doc.clone(), n.clone());
|
||||
let job: ThumbJob = (key, Arc::downgrade(n), px);
|
||||
if w.submit(key, job, Box::new(move |r| r.render_node_thumbnail_with(&doc, &node, px, None, &thumb_options()))) {
|
||||
self.pending.insert(key, (Arc::downgrade(n), px));
|
||||
return shown;
|
||||
}
|
||||
self.worker = None;
|
||||
self.pending.clear();
|
||||
}
|
||||
if !in_budget {
|
||||
ctx.request_repaint();
|
||||
return shown;
|
||||
}
|
||||
thread_local! {
|
||||
static RENDERER: std::cell::RefCell<vectorcraft_render::Renderer> = std::cell::RefCell::new(vectorcraft_render::Renderer::new());
|
||||
}
|
||||
let t0 = crate::now_ms();
|
||||
let img = RENDERER.with(|r| r.borrow_mut().render_node_thumbnail_with(src.doc, n, px, None, &thumb_options()));
|
||||
let ms = crate::now_ms() - t0;
|
||||
self.spent.1 += ms;
|
||||
self.spent.2 += 1;
|
||||
self.renders += 1;
|
||||
self.upload(ctx, key, Arc::downgrade(n), px, img.as_ref(), ms);
|
||||
self.entries.get(&key).and_then(|e| e.tex.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// A real rendered thumbnail of row `n`, cached by node identity (unchanged nodes keep their `Arc`
|
||||
/// allocation, so the address is a free change detector). Only rendered for visible rows; see
|
||||
/// [`Thumbs::texture`] for when.
|
||||
pub(crate) fn real_thumb(ui: &Ui, src: &ThumbSource, n: &Arc<Node>, r: egui::Rect) -> bool {
|
||||
if !ui.is_rect_visible(r) {
|
||||
return true;
|
||||
}
|
||||
let key = (n as *const Node as usize, n.id.0);
|
||||
let ppp = ui.ctx().pixels_per_point();
|
||||
let px = (r.width() * ppp).round() as u32;
|
||||
let tex = CACHE.with(|c| c.borrow().get(&key).cloned()).or_else(|| {
|
||||
let img = RENDERER.with(|rr| rr.borrow_mut().render_thumbnail(doc, n.id, px.max(8)))?;
|
||||
let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
|
||||
let tex = ui.ctx().load_texture(format!("layer-thumb-{}", n.id.0), color, egui::TextureOptions::LINEAR);
|
||||
CACHE.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
if c.len() > 2000 {
|
||||
c.clear();
|
||||
}
|
||||
c.insert(key, tex.clone());
|
||||
});
|
||||
Some(tex)
|
||||
});
|
||||
match tex {
|
||||
let px = ((r.width() * ppp).round() as u32).max(8);
|
||||
match THUMBS.with(|c| c.borrow_mut().texture(ui.ctx(), src, n, px)) {
|
||||
Some(t) => {
|
||||
ui.painter().image(t.id(), r, egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), Color32::WHITE);
|
||||
true
|
||||
|
|
@ -1303,6 +1488,34 @@ mod tests {
|
|||
(app, layer, [NodeId(a), NodeId(b)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_thumbnails_wait_for_the_end_of_a_drag() {
|
||||
let (mut app, _, _) = two_rects();
|
||||
app.session.execute("select.all", &json!({})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let renders = || THUMBS.with(|c| c.borrow().renders);
|
||||
frame(&mut app, &ctx, vec![], false);
|
||||
let first = renders();
|
||||
assert!(first >= 1, "the rows' thumbnails render at once");
|
||||
frame(&mut app, &ctx, vec![], false);
|
||||
assert_eq!(renders(), first, "and stay");
|
||||
// A move drag: the layer (and the moved objects) change on every preview.
|
||||
app.session.begin_interaction("Move").unwrap();
|
||||
for i in 1..=10 {
|
||||
app.session.preview("object.transform", &json!({"matrix": [1, 0, 0, 1, i, 0]})).unwrap();
|
||||
frame(&mut app, &ctx, vec![], false);
|
||||
}
|
||||
assert_eq!(renders(), first, "nothing renders while the drag goes on");
|
||||
app.session.commit_interaction().unwrap();
|
||||
for _ in 0..12 {
|
||||
frame(&mut app, &ctx, vec![], false);
|
||||
}
|
||||
assert!(renders() > first, "the changed rows render after it");
|
||||
let after = renders();
|
||||
frame(&mut app, &ctx, vec![], false);
|
||||
assert_eq!(renders(), after, "once");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicking_a_target_circle_targets_the_layer() {
|
||||
let (mut app, layer, [a, b]) = two_rects();
|
||||
|
|
|
|||
|
|
@ -234,7 +234,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
widgets::dim_label(ui, tl!("No document"));
|
||||
return;
|
||||
};
|
||||
let Some(doc) = app.session.active().map(|st| st.doc.clone()) else { return };
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let doc = st.doc.clone();
|
||||
let thumbs = super::layers::ThumbSource::of(app, st, &doc);
|
||||
let selected = selected_images(app);
|
||||
let rows: &[Value] = cache.list["links"].as_array().map_or(&[], Vec::as_slice);
|
||||
let mut clicked: Option<(u64, bool, bool)> = None;
|
||||
|
|
@ -267,8 +269,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
if o.thumb > 0.0 {
|
||||
let tr = egui::Rect::from_min_size(egui::pos2(x, rect.center().y - o.thumb / 2.0), vec2(o.thumb, o.thumb));
|
||||
ui.painter().rect_filled(tr, 0.0, egui::Color32::WHITE);
|
||||
if let Some(n) = doc.node(NodeId(id)) {
|
||||
super::layers::real_thumb(ui, &doc, n, tr);
|
||||
if let Some(n) = super::layers::node_arc(&doc, NodeId(id)) {
|
||||
super::layers::real_thumb(ui, &thumbs, &n, tr);
|
||||
}
|
||||
ui.painter().rect_stroke(tr, 0.0, egui::Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
x = tr.right() + 6.0;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
//! Navigator panel: a thumbnail of the document (rendered with `Renderer::render_region`, cached by
|
||||
//! document revision) with the red view box you drag to pan, zoom out/in buttons, a zoom slider
|
||||
//! Navigator panel: a thumbnail of the document (rendered with `Renderer::render_region_with`,
|
||||
//! cached by document revision, off the UI thread for heavy documents and not during drags: see
|
||||
//! [`crate::thumb_worker`]) with the red view box you drag to pan, zoom out/in buttons, a zoom slider
|
||||
//! and a zoom field.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::{Color32, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
use serde_json::json;
|
||||
use vectorcraft_geom::{Point, Rect as DRect};
|
||||
|
|
@ -9,19 +12,135 @@ use vectorcraft_geom::{Point, Rect as DRect};
|
|||
use super::{pstate, set_pstate};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::theme::Tokens;
|
||||
use crate::thumb_worker::{Settle, ThumbWorker};
|
||||
use crate::widgets::{self, menu_item};
|
||||
|
||||
/// What a navigator image shows: document `uid` at `revision`, `w`×`h` pixels, the artboards only.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct ImageKey {
|
||||
uid: u64,
|
||||
revision: u64,
|
||||
w: u32,
|
||||
h: u32,
|
||||
artboard_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Cache {
|
||||
renderer: Option<vectorcraft_render::Renderer>,
|
||||
key: Option<(usize, u64, u32, u32, bool)>,
|
||||
/// Renders heavy documents' images (None: on this thread).
|
||||
worker: Option<ThumbWorker<(), (ImageKey, DRect)>>,
|
||||
worker_started: bool,
|
||||
/// The region shown for (uid, revision, artboard only): the art bounds walk the whole document.
|
||||
region: Option<((u64, u64, bool), DRect)>,
|
||||
/// The image shown: what it shows and the document rect it covers.
|
||||
shown: Option<(ImageKey, DRect)>,
|
||||
tex: Option<egui::TextureHandle>,
|
||||
/// The image the worker is making.
|
||||
pending: Option<ImageKey>,
|
||||
/// How long the last image took: past [`HEAVY_MS`] the next goes to the worker.
|
||||
last_ms: f64,
|
||||
settle: Settle,
|
||||
/// Images made (tests count them).
|
||||
renders: u64,
|
||||
}
|
||||
|
||||
/// An image that took longer than this (ms) makes the next one render off the UI thread.
|
||||
const HEAVY_MS: f64 = 8.0;
|
||||
|
||||
thread_local! {
|
||||
static CACHE: crate::graphics::TexCache<Cache> = crate::graphics::TexCache::default();
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
/// Upload image `r` showing `key` over `region`.
|
||||
fn upload(&mut self, ctx: &egui::Context, key: ImageKey, region: DRect, r: &vectorcraft_render::Rendered, ms: f64) {
|
||||
let img = egui::ColorImage::from_rgba_premultiplied([r.width as usize, r.height as usize], &r.pixels);
|
||||
match &mut self.tex {
|
||||
Some(tx) => tx.set(img, egui::TextureOptions::LINEAR),
|
||||
None => self.tex = Some(ctx.load_texture("navigator", img, egui::TextureOptions::LINEAR)),
|
||||
}
|
||||
self.shown = Some((key, region));
|
||||
self.last_ms = ms;
|
||||
self.renders += 1;
|
||||
}
|
||||
|
||||
/// The region (artboards, and the art unless `artboard_only`, with a margin) to show
|
||||
/// `doc` (`uid` at `revision`) in. While `busy` (a drag) the last one of the document stays,
|
||||
/// rather than walking the whole document every frame.
|
||||
fn region(&mut self, doc: &vectorcraft_doc::Document, uid: u64, revision: u64, artboard_only: bool, busy: bool) -> DRect {
|
||||
let key = (uid, revision, artboard_only);
|
||||
if let Some((k, r)) = self.region
|
||||
&& (k == key || (busy && k.0 == uid && k.2 == artboard_only))
|
||||
{
|
||||
return r;
|
||||
}
|
||||
let mut region = doc.artboards.iter().map(|a| a.rect).reduce(|a, b| a.union(b)).unwrap_or(DRect::new(0.0, 0.0, 612.0, 792.0));
|
||||
if !artboard_only && let Some(b) = doc.art_bounds() {
|
||||
region = region.union(b);
|
||||
}
|
||||
let region = region.inflate(region.width() * 0.04 + 4.0, region.height() * 0.04 + 4.0);
|
||||
self.region = Some((key, region));
|
||||
region
|
||||
}
|
||||
|
||||
/// Bring the image up to `key` (`doc` over `region`): heavy documents render on the worker
|
||||
/// while the last image stays, and nothing renders during a drag (`busy`) or before the
|
||||
/// document has settled, unless there's no image of it yet.
|
||||
fn update(&mut self, ctx: &egui::Context, doc: &Arc<vectorcraft_doc::Document>, key: ImageKey, region: DRect, busy: bool) {
|
||||
if !self.worker_started {
|
||||
self.worker_started = true;
|
||||
self.worker = ThumbWorker::spawn(ctx, "navigator");
|
||||
}
|
||||
let done = match self.worker.as_ref().map(ThumbWorker::poll) {
|
||||
Some(Some(done)) => done,
|
||||
Some(None) => {
|
||||
// The thread is gone: render here from now on.
|
||||
(self.worker, self.pending) = (None, None);
|
||||
vec![]
|
||||
}
|
||||
None => vec![],
|
||||
};
|
||||
for d in done {
|
||||
if self.pending == Some(d.key.0) {
|
||||
self.pending = None;
|
||||
}
|
||||
if let Some(img) = d.img {
|
||||
self.upload(ctx, d.key.0, d.key.1, &img, d.ms);
|
||||
}
|
||||
}
|
||||
let shown = self.shown.map(|(k, _)| k);
|
||||
if shown == Some(key) || self.pending == Some(key) {
|
||||
return;
|
||||
}
|
||||
let have = self.tex.is_some() && shown.is_some_and(|k| k.uid == key.uid);
|
||||
let now = ctx.input(|i| i.time);
|
||||
if have && (busy || !self.settle.settled(ctx, key.uid, key.revision, now)) {
|
||||
return;
|
||||
}
|
||||
let s = (key.w as f64 / region.width()).min(key.h as f64 / region.height());
|
||||
// As exported (template layers and guides left out), transparent; images far smaller than
|
||||
// they are sample their mip levels.
|
||||
let opts = vectorcraft_render::RenderOptions { skip_templates: true, image_mips: true, ..Default::default() };
|
||||
let heavy = self.last_ms > HEAVY_MS && have;
|
||||
if heavy && let Some(w) = &self.worker {
|
||||
let (doc, opts) = (doc.clone(), opts.clone());
|
||||
if w.submit((), (key, region), Box::new(move |r| Some(r.render_region_with(&doc, region, s, &opts)))) {
|
||||
self.pending = Some(key);
|
||||
return;
|
||||
}
|
||||
self.worker = None;
|
||||
}
|
||||
let t0 = crate::now_ms();
|
||||
let img = NAV_RENDERER.with(|r| r.borrow_mut().render_region_with(doc, region, s, &opts));
|
||||
self.upload(ctx, key, region, &img, crate::now_ms() - t0);
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// Renders light documents' images on the UI thread.
|
||||
static NAV_RENDERER: std::cell::RefCell<vectorcraft_render::Renderer> = std::cell::RefCell::new(vectorcraft_render::Renderer::new());
|
||||
}
|
||||
|
||||
/// Map a document point into the thumbnail rect (region → rect, aspect preserved).
|
||||
pub fn doc_to_thumb(region: DRect, thumb: Rect, p: Point) -> egui::Pos2 {
|
||||
let s = (thumb.width() as f64 / region.width()).min(thumb.height() as f64 / region.height());
|
||||
|
|
@ -54,33 +173,25 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
};
|
||||
let artboard_only: bool = pstate(ui.ctx(), "nav-ab-only");
|
||||
let doc = st.doc.clone();
|
||||
let rev = st.revision;
|
||||
let doc_idx = app.session.documents().iter().position(|d| std::sync::Arc::ptr_eq(&d.doc, &doc)).unwrap_or(0);
|
||||
let mut region = doc.artboards.iter().map(|a| a.rect).reduce(|a, b| a.union(b)).unwrap_or(DRect::new(0.0, 0.0, 612.0, 792.0));
|
||||
if !artboard_only && let Some(b) = doc.art_bounds() {
|
||||
region = region.union(b);
|
||||
}
|
||||
let region = region.inflate(region.width() * 0.04 + 4.0, region.height() * 0.04 + 4.0);
|
||||
let (uid, revision) = (st.uid, st.revision);
|
||||
// A drag in progress: every pointer move makes a new revision. The image waits for its end.
|
||||
let busy = st.interaction.is_some() || app.session.tool_busy();
|
||||
let region = CACHE.with(|c| c.borrow_mut().region(&doc, uid, revision, artboard_only, busy));
|
||||
let w = ui.available_width();
|
||||
let h = (w * 0.72).clamp(100.0, 200.0);
|
||||
let (thumb, resp) = ui.allocate_exact_size(vec2(w, h), Sense::click_and_drag());
|
||||
ui.painter().rect_filled(thumb, 0.0, t.pasteboard);
|
||||
let ppp = ui.ctx().pixels_per_point();
|
||||
let (pw, ph) = ((w * ppp) as u32, (h * ppp) as u32);
|
||||
let key = (doc_idx, rev, pw, ph, artboard_only);
|
||||
let tex = CACHE.with(|c| {
|
||||
let key = ImageKey { uid, revision, w: pw, h: ph, artboard_only };
|
||||
let (tex, tex_region) = CACHE.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
if c.key != Some(key) || c.tex.is_none() {
|
||||
let s = (pw as f64 / region.width()).min(ph as f64 / region.height());
|
||||
let r = c.renderer.get_or_insert_with(vectorcraft_render::Renderer::new).render_region(&doc, region, s, false);
|
||||
let img = egui::ColorImage::from_rgba_premultiplied([r.width as usize, r.height as usize], &r.pixels);
|
||||
match &mut c.tex {
|
||||
Some(tx) => tx.set(img, egui::TextureOptions::LINEAR),
|
||||
None => c.tex = Some(ui.ctx().load_texture("navigator", img, egui::TextureOptions::LINEAR)),
|
||||
}
|
||||
c.key = Some(key);
|
||||
c.update(ui.ctx(), &doc, key, region, busy);
|
||||
// Never another document's image.
|
||||
match c.shown {
|
||||
Some((k, r)) if k.uid == uid => (c.tex.clone(), r),
|
||||
_ => (None, region),
|
||||
}
|
||||
c.tex.clone()
|
||||
});
|
||||
// Artboards (white) under the art thumbnail, like the canvas.
|
||||
for a in &doc.artboards {
|
||||
|
|
@ -88,8 +199,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
ui.painter().rect_filled(r, 0.0, Color32::WHITE);
|
||||
}
|
||||
if let Some(tex) = tex {
|
||||
let tl = doc_to_thumb(region, thumb, region.origin());
|
||||
let br = doc_to_thumb(region, thumb, Point::new(region.x1, region.y1));
|
||||
let tl = doc_to_thumb(region, thumb, tex_region.origin());
|
||||
let br = doc_to_thumb(region, thumb, Point::new(tex_region.x1, tex_region.y1));
|
||||
ui.painter().image(tex.id(), Rect::from_min_max(tl, br), Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||
}
|
||||
// The view box.
|
||||
|
|
@ -164,4 +275,42 @@ mod tests {
|
|||
assert!((zoom_to_slider(64000.0) - 1.0).abs() < 1e-9);
|
||||
assert!((slider_to_zoom(zoom_to_slider(100.0)) - 100.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// The navigator images made on this thread so far.
|
||||
fn renders() -> u64 {
|
||||
CACHE.with(|c| c.borrow().renders)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_image_is_rendered_during_a_drag_and_one_is_after_it() {
|
||||
use vectorcraft_engine::Session;
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
app.session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||||
// One context, so its clock runs on (1/60 s a pass) and the document can settle.
|
||||
let ctx = egui::Context::default();
|
||||
let frame = |app: &mut VectorcraftApp| {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
frame(&mut app);
|
||||
let first = renders();
|
||||
assert!(first >= 1, "the first image renders at once");
|
||||
// A move drag: every preview is a new revision.
|
||||
app.session.begin_interaction("Move").unwrap();
|
||||
for i in 1..=20 {
|
||||
app.session.preview("object.transform", &json!({"matrix": [1, 0, 0, 1, i, 0]})).unwrap();
|
||||
frame(&mut app);
|
||||
}
|
||||
assert_eq!(renders(), first, "nothing renders while the drag goes on");
|
||||
app.session.commit_interaction().unwrap();
|
||||
frame(&mut app);
|
||||
assert_eq!(renders(), first, "the document settles first");
|
||||
for _ in 0..12 {
|
||||
frame(&mut app);
|
||||
}
|
||||
assert_eq!(renders(), first + 1, "one image after the drag");
|
||||
let shown = CACHE.with(|c| c.borrow().shown.map(|(k, _)| k.revision));
|
||||
assert_eq!(shown, Some(app.session.active().unwrap().revision));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,7 +248,8 @@ fn mask_items(mask: Option<&OpacityMask>, can_make: bool) -> [(&'static str, &'s
|
|||
}
|
||||
|
||||
/// A rendered thumbnail of `n` (which may live outside the tree, like mask art). One texture per
|
||||
/// slot, re-rendered only when the document revision, object or size changes.
|
||||
/// slot, re-rendered only when the document revision, object or size changes, and not during a
|
||||
/// drag (every pointer move is a new revision): the last one stays until it ends.
|
||||
fn node_thumb(app: &VectorcraftApp, ui: &Ui, slot: &str, n: &vectorcraft_doc::Node, r: egui::Rect, bg: Option<[u8; 4]>) -> bool {
|
||||
use std::cell::RefCell;
|
||||
thread_local! {
|
||||
|
|
@ -256,14 +257,15 @@ fn node_thumb(app: &VectorcraftApp, ui: &Ui, slot: &str, n: &vectorcraft_doc::No
|
|||
}
|
||||
let Some(st) = app.session.active() else { return false };
|
||||
let px = (r.width().min(r.height()) * ui.ctx().pixels_per_point()).round().max(8.0) as u32;
|
||||
let (slot_id, key) = (egui::Id::new(slot), egui::Id::new((st.uid, st.revision, n.id, bg, px)));
|
||||
let cached: Option<(egui::Id, egui::TextureHandle)> = ui.ctx().data(|d| d.get_temp(slot_id));
|
||||
let (slot_id, key) = (egui::Id::new(slot), egui::Id::new((st.uid, n.id, bg, px)));
|
||||
let busy = st.interaction.is_some() || app.session.tool_busy();
|
||||
let cached: Option<(egui::Id, u64, egui::TextureHandle)> = ui.ctx().data(|d| d.get_temp(slot_id));
|
||||
let tex = match cached {
|
||||
Some((k, tex)) if k == key => Some(tex),
|
||||
_ => RENDERER.with(|rr| rr.borrow_mut().render_node_thumbnail(&st.doc, n, px, bg)).map(|img| {
|
||||
Some((k, rev, tex)) if k == key && (rev == st.revision || busy) => Some(tex),
|
||||
_ => RENDERER.with(|rr| rr.borrow_mut().render_node_thumbnail_with(&st.doc, n, px, bg, &super::layers::thumb_options())).map(|img| {
|
||||
let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
|
||||
let tex = ui.ctx().load_texture(slot, color, egui::TextureOptions::LINEAR);
|
||||
ui.ctx().data_mut(|d| d.insert_temp(slot_id, (key, tex.clone())));
|
||||
ui.ctx().data_mut(|d| d.insert_temp(slot_id, (key, st.revision, tex.clone())));
|
||||
tex
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
//! Panel thumbnails (the Navigator's view of the document, the Layers and Links rows) rendered
|
||||
//! off the UI thread on native builds, so a heavy document never stalls a frame. The panels keep
|
||||
//! showing their last image until the new one is back, and leave thumbnails alone while the user
|
||||
//! drags (every pointer move makes a new document revision): they update once the drag ends and
|
||||
//! the document has [`Settle`]d.
|
||||
//!
|
||||
//! On wasm (no threads) and in unit tests [`ThumbWorker::spawn`] gives `None` and the panels
|
||||
//! render on the UI thread, with the same rules about drags and settling.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
|
||||
|
||||
use vectorcraft_render::{Rendered, Renderer};
|
||||
|
||||
/// How long (seconds) the document must stay unchanged before a thumbnail that is already shown
|
||||
/// is drawn again.
|
||||
pub const SETTLE_SECS: f64 = 0.1;
|
||||
|
||||
/// A render a panel asks for: run on the worker's own [`Renderer`].
|
||||
pub type JobFn = Box<dyn FnOnce(&mut Renderer) -> Option<Rendered> + Send>;
|
||||
|
||||
/// A finished job: the key it was submitted with, its image and how long it took.
|
||||
pub struct Done<K> {
|
||||
pub key: K,
|
||||
pub img: Option<Rendered>,
|
||||
pub ms: f64,
|
||||
}
|
||||
|
||||
struct Job<S, K> {
|
||||
slot: S,
|
||||
key: K,
|
||||
run: JobFn,
|
||||
}
|
||||
|
||||
/// A render thread for one panel's thumbnails. Jobs go in by slot (the Navigator has one, the
|
||||
/// Layers panel one per row): a job not started yet is replaced by a newer one for its slot, so
|
||||
/// only the latest image of anything is drawn.
|
||||
pub struct ThumbWorker<S, K> {
|
||||
tx: Sender<Job<S, K>>,
|
||||
rx: Receiver<Done<K>>,
|
||||
}
|
||||
|
||||
impl<S: PartialEq + Send + 'static, K: Send + 'static> ThumbWorker<S, K> {
|
||||
/// Spawn the worker thread, `None` where rendering stays on the UI thread: on wasm, in unit
|
||||
/// tests (they look at the panel right after the frame that asks), with
|
||||
/// `VECTORCRAFT_SYNC_RENDER` set, or if the thread can't start.
|
||||
pub fn spawn(ctx: &egui::Context, name: &str) -> Option<Self> {
|
||||
if cfg!(target_arch = "wasm32") || cfg!(test) || std::env::var_os("VECTORCRAFT_SYNC_RENDER").is_some() {
|
||||
return None;
|
||||
}
|
||||
Self::start(ctx, name)
|
||||
}
|
||||
|
||||
/// Spawn the thread.
|
||||
fn start(ctx: &egui::Context, name: &str) -> Option<Self> {
|
||||
let (tx, jobs) = channel::<Job<S, K>>();
|
||||
let (done_tx, rx) = channel::<Done<K>>();
|
||||
let ctx = ctx.clone();
|
||||
std::thread::Builder::new()
|
||||
.name(format!("vectorcraft-{name}"))
|
||||
.spawn(move || {
|
||||
let mut r = Renderer::new();
|
||||
let mut queue: VecDeque<Job<S, K>> = VecDeque::new();
|
||||
let add = |queue: &mut VecDeque<Job<S, K>>, job: Job<S, K>| match queue.iter_mut().find(|j| j.slot == job.slot) {
|
||||
Some(old) => *old = job,
|
||||
None => queue.push_back(job),
|
||||
};
|
||||
loop {
|
||||
if queue.is_empty() {
|
||||
// The panel is gone (its sender dropped): end the thread.
|
||||
let Ok(job) = jobs.recv() else { break };
|
||||
add(&mut queue, job);
|
||||
}
|
||||
while let Ok(job) = jobs.try_recv() {
|
||||
add(&mut queue, job);
|
||||
}
|
||||
let Some(job) = queue.pop_front() else { continue };
|
||||
let t0 = crate::now_ms();
|
||||
let img = (job.run)(&mut r);
|
||||
if done_tx.send(Done { key: job.key, img, ms: crate::now_ms() - t0 }).is_err() {
|
||||
break;
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
Some(Self { tx, rx })
|
||||
}
|
||||
|
||||
/// Queue `run` for `slot` (replacing a job for the slot that hasn't started). False when the
|
||||
/// thread is gone: the caller renders on the UI thread from now on.
|
||||
#[must_use]
|
||||
pub fn submit(&self, slot: S, key: K, run: JobFn) -> bool {
|
||||
self.tx.send(Job { slot, key, run }).is_ok()
|
||||
}
|
||||
|
||||
/// The jobs finished since the last call, oldest first; None when the thread is gone (the
|
||||
/// jobs it had will never finish).
|
||||
pub fn poll(&self) -> Option<Vec<Done<K>>> {
|
||||
let mut done = vec![];
|
||||
loop {
|
||||
match self.rx.try_recv() {
|
||||
Ok(d) => done.push(d),
|
||||
Err(TryRecvError::Empty) => return Some(done),
|
||||
Err(TryRecvError::Disconnected) => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// When the document last changed, for [`SETTLE_SECS`]: thumbnails already shown are drawn again
|
||||
/// only once it has stayed the same that long.
|
||||
#[derive(Default)]
|
||||
pub struct Settle {
|
||||
seen: Option<(u64, u64)>,
|
||||
since: f64,
|
||||
}
|
||||
|
||||
impl Settle {
|
||||
/// Note that document `uid` is at `revision` at time `now` (seconds, egui's input time) →
|
||||
/// whether it has stayed there [`SETTLE_SECS`]. Otherwise `ctx` is asked to draw again when
|
||||
/// it will have.
|
||||
pub fn settled(&mut self, ctx: &egui::Context, uid: u64, revision: u64, now: f64) -> bool {
|
||||
if self.seen != Some((uid, revision)) {
|
||||
self.seen = Some((uid, revision));
|
||||
self.since = now;
|
||||
}
|
||||
let waited = now - self.since;
|
||||
// The clock went back (a new egui context): don't wait for it to catch up.
|
||||
if waited >= SETTLE_SECS || waited < 0.0 {
|
||||
return true;
|
||||
}
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f64((SETTLE_SECS - waited).max(0.0)));
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn jobs_come_back_newest_last_per_slot() {
|
||||
let Some(w) = ThumbWorker::<u8, u32>::start(&egui::Context::default(), "test") else { return };
|
||||
let job = |n: u32| -> JobFn { Box::new(move |_| Some(Rendered { width: n, height: 1, pixels: vec![0; 4] })) };
|
||||
for k in 1..=3 {
|
||||
assert!(w.submit(0, k, job(k)));
|
||||
}
|
||||
assert!(w.submit(1, 10, job(10)));
|
||||
let mut got = vec![];
|
||||
let t0 = std::time::Instant::now();
|
||||
while !(got.contains(&3) && got.contains(&10)) && t0.elapsed().as_secs() < 30 {
|
||||
got.extend(w.poll().unwrap().into_iter().map(|d| {
|
||||
assert_eq!(d.img.map(|i| i.width), Some(d.key));
|
||||
d.key
|
||||
}));
|
||||
std::thread::yield_now();
|
||||
}
|
||||
let slot0: Vec<u32> = got.iter().copied().filter(|k| *k < 10).collect();
|
||||
assert_eq!(slot0.last(), Some(&3), "the newest job of a slot is the last done: {got:?}");
|
||||
assert!(slot0.windows(2).all(|p| p[0] < p[1]), "{got:?}");
|
||||
assert!(got.contains(&10));
|
||||
drop(w);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settles_after_the_document_stays_unchanged() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut s = Settle::default();
|
||||
assert!(!s.settled(&ctx, 1, 1, 10.0), "a change starts the wait");
|
||||
assert!(!s.settled(&ctx, 1, 1, 10.05));
|
||||
assert!(s.settled(&ctx, 1, 1, 10.15));
|
||||
assert!(!s.settled(&ctx, 1, 2, 10.2), "a new revision starts it again");
|
||||
assert!(!s.settled(&ctx, 2, 2, 10.25), "so does another document");
|
||||
assert!(s.settled(&ctx, 2, 2, 1.0), "a clock that went back doesn't hold it up");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue