mail client (IMAP/SMTP)
git clone https://git.lucas.co/cce-mail.git
The rendered message belongs to a renderer, not to the process
An image id names an entry in one renderer's image table, and a renderer
does not outlive its session: cce-ui's window_runner repairs a lost
Wayland transport by opening a new session around the same Application,
which rebuilds the VkRenderer and with it the image table. A draw for an
id that table does not hold is skipped rather than reported.
The detail pane draws the WPE page as such an id, and MailWebView only
replaces it when WPE produces a NEW frame — which a page that has
finished loading never does. So a reconnect left the folder list, the
message list and the headers intact beside an empty body pane, for as
long as the message stayed selected.
MailWebView already keeps last_frame, so the repair costs nothing but an
upload: reupload_frame() frees the stale id, uploads those pixels under
a fresh one, and writes self.image — the field pump owns — so the next
real frame still frees the right image. No reload, no WPE round trip, no
refetch of remote content; the page is back on the next frame.
The app calls it from a new renderer_init, on every renderer after the
first. The first is the one the uploads already queued are waiting for.
This is the shape cce-status-interface landed in 73ab926 and cce-grid
has had for a while; window_runner documents the contract above `run`.
src/main.rs | 33 +++++++++++++++++++++++++++++++++
src/wpe/host.rs | 35 ++++++++++++++++++++++++++++++++++-
2 files changed, 67 insertions(+), 1 deletion(-)
diff --git a/src/main.rs b/src/main.rs
index 8141e50..09612a2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -284,6 +284,10 @@ struct ClearEmailApp {
// only a 1200-char text preview.
#[cfg(feature = "wpe")]
webview: wpe::MailWebView,
+ /// Whether a renderer has been handed over yet — the first one is the
+ /// process's own, any later one is a replacement after a reconnect. See
+ /// `renderer_init`.
+ seen_renderer: bool,
/// Email id whose HTML the webview currently shows.
#[cfg(feature = "wpe")]
html_loaded: Option<usize>,
@@ -3966,6 +3970,7 @@ impl Application for ClearEmailApp {
body_sb_activity: cce_ui::widget::ScrollbarActivity::new(),
#[cfg(feature = "wpe")]
webview: wpe::MailWebView::new((800, 600)),
+ seen_renderer: false,
#[cfg(feature = "wpe")]
html_loaded: None,
#[cfg(feature = "wpe")]
@@ -4644,6 +4649,34 @@ impl Application for ClearEmailApp {
}
}
+ /// Hand the rendered message back to a replacement renderer.
+ ///
+ /// The detail pane draws the WPE page as an image id, and an id belongs to
+ /// a renderer, not to the process: `cce-ui`'s `window_runner` repairs a
+ /// lost Wayland transport by opening a new session around the same
+ /// `Application`, which rebuilds the renderer and with it the image table.
+ /// A draw for an unknown id is skipped rather than reported, and the
+ /// webview only re-uploads when WPE produces a new frame — which a
+ /// finished page never does. So a reconnect left the message list and the
+ /// headers intact beside an empty body pane.
+ ///
+ /// `reupload_frame` replays the pixels the webview already holds, so the
+ /// page comes back on the next frame with no reload and no refetch of
+ /// remote content.
+ ///
+ /// Not on the first renderer: nothing has been uploaded yet, and the
+ /// webview has not rendered a frame.
+ fn renderer_init(&mut self, _renderer: &mut cce_ui::vk::VkRenderer) {
+ if !std::mem::replace(&mut self.seen_renderer, true) {
+ return;
+ }
+ #[cfg(feature = "wpe")]
+ if self.webview.reupload_frame() {
+ eprintln!("cce-mail: renderer replaced; re-uploaded the rendered message");
+ self.needs_rebuild = true;
+ }
+ }
+
fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<cce_ui::scene::paint::DisplayList> {
self.register_dispatch_roots();
// Phase 6ai single paint path: the view() geometry (all plain quads) and the text
diff --git a/src/wpe/host.rs b/src/wpe/host.rs
index 51822f1..cca8ec8 100644
--- a/src/wpe/host.rs
+++ b/src/wpe/host.rs
@@ -99,7 +99,12 @@ pub struct MailWebView {
images_allowed: bool,
/// Last uploaded frame in the image registry: (id, w px, h px).
image: Option<(u32, u32, u32)>,
- /// Retained so the headless example can assert on rendered output.
+ /// The pixels behind [`image`], kept so the frame can be handed to a
+ /// replacement renderer after a reconnect (see [`reupload_frame`]) — and
+ /// so the headless example can assert on rendered output.
+ ///
+ /// [`image`]: MailWebView::image
+ /// [`reupload_frame`]: MailWebView::reupload_frame
last_frame: Option<(Vec<u8>, u32, u32)>,
}
@@ -404,6 +409,34 @@ impl MailWebView {
self.image
}
+ /// Hand the last frame to a renderer that has just replaced the one it
+ /// was uploaded to. Returns true when the pane should repaint.
+ ///
+ /// An image id belongs to a **renderer**, and a renderer does not outlive
+ /// its session: `cce-ui`'s `window_runner` repairs a lost Wayland
+ /// transport by opening a new session around the same `Application`,
+ /// which rebuilds the renderer and with it the image table. A draw for an
+ /// unknown id is skipped rather than reported, and `self.image` is only
+ /// replaced when WPE produces a NEW frame — so a message that had
+ /// finished loading (the normal case: a page is painted once and then sits
+ /// there) would show an empty detail pane until something forced a
+ /// reload.
+ ///
+ /// Re-uploading the pixels beats re-rendering: no WPE round trip, no
+ /// refetch of remote content, and the pane comes back on the very next
+ /// frame. The new id is written to `self.image`, the field `pump` owns, so
+ /// the next real frame still frees the right one.
+ pub fn reupload_frame(&mut self) -> bool {
+ if let Some((old, ..)) = self.image.take() {
+ // A free for an id the new renderer never had is a no-op, and ids
+ // are process-unique, so this cannot reach a live image.
+ cce_ui::vk::free_image(old);
+ }
+ let Some((px, w, h)) = self.last_frame.clone() else { return false };
+ self.image = Some((cce_ui::vk::upload_rgba(px, w, h), w, h));
+ true
+ }
+
/// A pixel of the last frame, for tests asserting on rendered output
/// (examples/wpe_mail.rs; dead in the app build).
#[allow(dead_code)]