Embed the plot in an egui app
The plot is a library widget, archiveview::Plot, and the archiveview
program is a thin window around it. Any egui 0.36 app can show one, and as
many as it likes; bobview is meant to use it for its trend windows.
[dependencies]
# default-features = false: just the widget, without the standalone app's
# eframe, png and clap
archiveview = { git = "https://github.com/gilesknap/archiveview", default-features = false }
pv = { package = "bobview-pv", version = "1.8" }
egui = "0.36.2"
The host must use the same pv crate as archiveview, the bobview-pv
package: Cargo treats two packages with different names as different
crates, even from the same source, so a PvManager from a path dependency
on a package called pv cannot be passed to Plot::new. bobview itself
therefore names its in-tree crate's package bobview-pv (keeping pv as
the name it is imported by) and points the published one at it:
# bobview's workspace Cargo.toml
[patch.crates-io]
bobview-pv = { path = "crates/pv" }
#![allow(unused)] fn main() { use archiveview::{ArchiverClient, Plot}; // once, in the app's constructor; share the app's own PvManager let pvs = pv::PvManager::new(); let archiver = ArchiverClient::new("http://archiver:17668").ok(); let mut plot = Plot::new(&pvs, archiver); plot.add_pvs(&["SR-DI-DCCT-01:SIGNAL".to_string()]); // each frame, e.g. inside an egui::Window: give it a size, as the plot // fills whatever space it is offered (at least 360 x 200) egui::Window::new(plot.title()) .id(egui::Id::new("trend-1")) .default_size([720.0, 420.0]) .resizable(true) .show(ctx, |ui| plot.ui(ui)); }
What the widget does, and doesn't, assume:
-
It uses the host's PV context.
Plot::newtakes the host'spv::PvManager, subscribes through it, and runs archive requests on that manager's tokio runtime. It starts no runtime or threads of its own. -
No archiver is fine. With
Noneit plots live values only, like bobview's trend window.set_archiverchanges it later. -
The archiver is the host's unless it says otherwise. The archiver pill always shows whether the archiver can be reached (
plot.archiver_status()), but by default it is a status display only, andset_viewignores a view'sarchiver, so a layout restored later never moves the plot to another archiver.plot.set_archiver_switching(true)(as the standalone app does) lets the user switch archivers in the pill and makesset_viewfollow a view's archiver. The plot then switches itself, andplot.take_archiver_change()is true once afterwards so the host can remember the choice; give it the host's remembered archivers withplot.set_recent_archivers(..). Persisting them is the host's job: the widget writes no files. -
No globals. Every plot keeps its own state and egui ids, so several can be open at once.
Plot::newsalts its ids with a running count;Plot::with_id_salt(&pvs, archiver, "trend-1")uses the host's own salt, which keeps them stable when the host rebuilds its plots. -
The host owns the look. The plot follows
ui.visuals()(light or dark) and the host's fonts; it never sets the theme or installs fonts. -
It repaints itself when archive data arrives (in the viewport it was last drawn in, so a plot in its own native window is repainted, not the root one) and, while following now, at the pace the time axis scrolls. A host whose every repaint redraws more than the plot can slow it:
plot.set_min_repaint_interval(Duration::from_millis(250))floors every repaint the plot schedules itself (the scroll pace, the Live pill's pulse, spinners, debounce and probe waits). Easing after the user's own pans and zooms, and the repaint when data arrives, stay immediate; with a floor, a live rescale of the value axes moves once per tick. The floor does not cover repaints from the host's ownPvManagernotifier on live monitor updates: those are the host's to pace. -
It reads no clipboard. Ctrl+V over the plot works on its own (egui delivers the clipboard as a paste event). For middle-click, the host reads the primary selection:
plot.take_middle_click()says where a middle-click landed, and the host answers with the selection's text,plot.paste_pvs(&text)on the plot orplot.paste_into_search(&text)in the search box.plot.take_search_selection()gives text the user selected in the search box, for the host to make the primary selection, andplot.take_pasted()says what a paste added (.message()for a toast). A middle-click stays intake_middle_click()until taken (a later one replaces it);paste_pvsandpaste_into_searchask for a repaint themselves, so the host can answer between frames, from another thread's result. Read the selection off the UI thread: X11 asks its owner, which can take seconds. The standalone app does all this inapp.rs, witharchiveview::selection(aWorkerthread, a 2 s limit, and setting the primary only once a selection has been steady for 150 ms).
To save a plot in the host's own layout files, store plot.view() (it is
serde-serialisable) and restore it with plot.set_view(&view). A view
records the archiver in use; set_view switches to it only with archiver
switching on.
plot.is_loading() is true while archive requests are in flight.
The plot keeps its own undo and redo lists (in memory, about 100 steps),
with ↶ ↷ buttons in its bar, Ctrl+Z and Ctrl+Shift+Z, and the mouse's
back and forward buttons. Restoring a layout with set_view, and adding
PVs with add_pvs, are steps too. A host with an undo of its own can turn
the plot's controls off with plot.set_undo_controls(false) and drive it
instead: plot.undo() and plot.redo() return whether there was a step,
and plot.undo_label() and plot.redo_label() name it ("zoom"). A host
with no use for them can stop the recording (and the memory it keeps,
such as removed traces' data) with plot.set_undo_enabled(false). Give the
name of the layout shown with plot.set_view_name(..): each step keeps it,
so undoing an opened layout gives back the name of the one before
(plot.view_name()). Undo never switches the archiver back.
The search box remembers recent searches in memory only. To keep them
across runs, as the standalone app does in history.json, give the plot
the saved list with plot.set_search_history(history) and save
plot.search_history() (a serde-serialisable SearchHistory) whenever
plot.search_history_changed() returns true. A host with several plots
can share one list by passing it between them the same way.
See the Rust API for the full interface.