mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-30 05:17:25 -06:00
Merge branch 'w3' into w6
This commit is contained in:
@@ -456,20 +456,29 @@ impl SurfaceGeometry {
|
||||
/// Derives this frame's capturable surface from raw viewport data, rejecting a missing, non-finite
|
||||
/// or zero-sized visible surface and recording minimized or occluded state.
|
||||
pub(crate) fn from_raw(raw: &egui::RawInput, ctx: &egui::Context) -> Option<Self> {
|
||||
let viewport = raw.viewports.get(&raw.viewport_id)?;
|
||||
Self::from_viewport(raw.viewports.get(&raw.viewport_id)?, raw.screen_rect, raw.max_texture_side, ctx)
|
||||
}
|
||||
|
||||
/// [`Self::from_raw`] for one viewport's info, content rectangle and texture limit.
|
||||
pub(crate) fn from_viewport(
|
||||
viewport: &egui::ViewportInfo,
|
||||
screen_rect: Option<egui::Rect>,
|
||||
max_texture_side: Option<usize>,
|
||||
ctx: &egui::Context,
|
||||
) -> Option<Self> {
|
||||
let minimized = viewport.minimized == Some(true);
|
||||
let occluded = viewport.occluded == Some(true);
|
||||
// A minimized/occluded surface may report no rectangle at all. Its stale size is harmless
|
||||
// because such a surface is never capturable; a visible surface without a rectangle is not
|
||||
// fresh enough to use.
|
||||
let rect = match viewport.inner_rect.or(raw.screen_rect) {
|
||||
let rect = match viewport.inner_rect.or(screen_rect) {
|
||||
Some(rect) => rect,
|
||||
None if minimized || occluded => ctx.content_rect(),
|
||||
None => return None,
|
||||
};
|
||||
let zoom = ctx.zoom_factor();
|
||||
let pixels_per_point = viewport.native_pixels_per_point.map_or_else(|| ctx.pixels_per_point(), |native| native * zoom);
|
||||
let max_texture_side = raw.max_texture_side.filter(|side| *side > 0).unwrap_or_else(|| ctx.input(|i| i.max_texture_side));
|
||||
let max_texture_side = max_texture_side.filter(|side| *side > 0).unwrap_or_else(|| ctx.input(|i| i.max_texture_side));
|
||||
if !rect.width().is_finite() || !rect.height().is_finite() || rect.width() <= 0.0 || rect.height() <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
@@ -479,9 +488,10 @@ impl SurfaceGeometry {
|
||||
Some(Self { viewport: rect.size(), pixels_per_point, max_texture_side, minimized, occluded })
|
||||
}
|
||||
|
||||
/// Pairs the surface geometry with the document id a capture belongs to.
|
||||
pub(crate) fn with_document(self, document: u64) -> CaptureGeometry {
|
||||
CaptureGeometry { surface: self, document }
|
||||
/// Pairs the surface geometry with the window's viewport and the document id a capture
|
||||
/// belongs to.
|
||||
pub(crate) fn with_target(self, viewport: egui::ViewportId, document: u64) -> CaptureGeometry {
|
||||
CaptureGeometry { surface: self, viewport, document }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,6 +501,8 @@ impl SurfaceGeometry {
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct CaptureGeometry {
|
||||
surface: SurfaceGeometry,
|
||||
/// The active window's viewport, which the capture command goes to.
|
||||
viewport: egui::ViewportId,
|
||||
document: u64,
|
||||
}
|
||||
|
||||
@@ -500,6 +512,18 @@ impl CaptureGeometry {
|
||||
self.document
|
||||
}
|
||||
|
||||
/// Why a capture admitted for `viewport` and `document` can't go on against this geometry:
|
||||
/// another window was brought forward, or the window shows another document.
|
||||
fn moved_reason(self, viewport: egui::ViewportId, document: u64) -> Option<&'static str> {
|
||||
if self.viewport != viewport {
|
||||
Some("screenshot failed: another window was brought forward while the capture was queued")
|
||||
} else if self.document != document {
|
||||
Some("screenshot failed: the document changed while the capture was queued")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Explains why the surface cannot be captured, when it is minimized or fully occluded, and
|
||||
/// returns `None` when it can.
|
||||
pub(crate) fn unavailable_reason(self) -> Option<&'static str> {
|
||||
@@ -569,6 +593,9 @@ pub(crate) struct Shot {
|
||||
pub(crate) retries: u32,
|
||||
/// The document the request was admitted for. It never changes; a document switch cancels.
|
||||
pub(crate) document: u64,
|
||||
/// The viewport of the window the request was admitted for. It never changes; bringing
|
||||
/// another window forward cancels.
|
||||
pub(crate) viewport: egui::ViewportId,
|
||||
pub(crate) geometry: CaptureGeometry,
|
||||
pub(crate) epoch: u64,
|
||||
pub(crate) expected: [usize; 2],
|
||||
@@ -623,6 +650,8 @@ fn next_token(app: &mut WordApp) -> u64 {
|
||||
/// earlier request.
|
||||
pub(crate) fn admit_screenshot(app: &mut WordApp, ctx: &egui::Context, path: Option<String>, reply: Sender<Value>) {
|
||||
let token = next_token(app);
|
||||
// An earlier request in this batch may have brought another window forward.
|
||||
app.refresh_capture_geometry(ctx);
|
||||
let Some(geometry) = app.capture_geometry.map(|g| CaptureGeometry { document: app.session.document_id(), ..g }) else {
|
||||
reject_shot(reply, token, "screenshot failed: the window geometry is not known yet");
|
||||
return;
|
||||
@@ -684,6 +713,7 @@ pub(crate) fn admit_screenshot(app: &mut WordApp, ctx: &egui::Context, path: Opt
|
||||
issued: false,
|
||||
retries: 0,
|
||||
document: geometry.document(),
|
||||
viewport: geometry.viewport,
|
||||
geometry,
|
||||
epoch: app.capture_epoch,
|
||||
expected,
|
||||
@@ -708,7 +738,8 @@ pub(crate) fn issue_screenshots(app: &mut WordApp, ctx: &egui::Context) {
|
||||
let shot = &app.shots.requests[i];
|
||||
(shot.issued, shot.ready_at, shot.frames)
|
||||
};
|
||||
if issued {
|
||||
// Each window issues its own captures, from a pass that drew its layout.
|
||||
if issued || app.shots.requests[i].viewport != ctx.viewport_id() {
|
||||
continue;
|
||||
}
|
||||
let frames = if first_pass { frames.saturating_add(1) } else { frames };
|
||||
@@ -724,8 +755,8 @@ pub(crate) fn issue_screenshots(app: &mut WordApp, ctx: &egui::Context) {
|
||||
failures.push((i, format!("screenshot failed: {reason} while the capture was queued")));
|
||||
continue;
|
||||
}
|
||||
if current.document() != app.shots.requests[i].document {
|
||||
failures.push((i, "screenshot failed: the document changed while the capture was queued".into()));
|
||||
if let Some(reason) = current.moved_reason(app.shots.requests[i].viewport, app.shots.requests[i].document) {
|
||||
failures.push((i, reason.into()));
|
||||
continue;
|
||||
}
|
||||
let Some(expected) = current.expected_size() else {
|
||||
@@ -762,7 +793,7 @@ pub(crate) fn issue_screenshots(app: &mut WordApp, ctx: &egui::Context) {
|
||||
shot.bytes = bytes;
|
||||
shot.epoch = epoch;
|
||||
if let Some(callback) = shot.callback.take() {
|
||||
ctx.send_viewport_cmd_to(shot.viewport(), egui::ViewportCommand::Screenshot(callback));
|
||||
ctx.send_viewport_cmd_to(shot.viewport, egui::ViewportCommand::Screenshot(callback));
|
||||
shot.issued = true;
|
||||
}
|
||||
}
|
||||
@@ -778,13 +809,6 @@ pub(crate) fn issue_screenshots(app: &mut WordApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Shot {
|
||||
/// The screenshot callback's viewport: the root window.
|
||||
fn viewport(&self) -> egui::ViewportId {
|
||||
egui::ViewportId::ROOT
|
||||
}
|
||||
}
|
||||
|
||||
/// A completed image is either stale because the surface changed (safe to retry once within the
|
||||
/// deadline) or invalid for this request (report it at once; retrying cannot help).
|
||||
enum ImageCheck {
|
||||
@@ -850,6 +874,13 @@ fn take_screenshot_action(app: &mut WordApp, index: usize, now: f64, current: Op
|
||||
}
|
||||
return ShotAction::Remove(error_reply(shot.token, "no frame was presented before the screenshot timeout"));
|
||||
}
|
||||
// Before the issue check: a window that closed never draws to issue its capture.
|
||||
if let Some(reason) = current.and_then(|geometry| geometry.moved_reason(shot.viewport, shot.document)) {
|
||||
if let Some(slot) = shot.slot.take() {
|
||||
slot.cancel();
|
||||
}
|
||||
return ShotAction::Remove(error_reply(shot.token, reason));
|
||||
}
|
||||
if !shot.issued {
|
||||
return ShotAction::Keep;
|
||||
}
|
||||
@@ -859,12 +890,6 @@ fn take_screenshot_action(app: &mut WordApp, index: usize, now: f64, current: Op
|
||||
}
|
||||
return ShotAction::Remove(error_reply(shot.token, format!("screenshot failed: {reason} while the capture was pending")));
|
||||
}
|
||||
if current.is_some_and(|geometry| geometry.document() != shot.document) {
|
||||
if let Some(slot) = shot.slot.take() {
|
||||
slot.cancel();
|
||||
}
|
||||
return ShotAction::Remove(error_reply(shot.token, "screenshot failed: the document changed while the capture was queued"));
|
||||
}
|
||||
shot.slot.as_ref().and_then(|slot| slot.take())
|
||||
};
|
||||
match completed {
|
||||
@@ -1080,7 +1105,7 @@ mod tests {
|
||||
|
||||
/// Builds a capture geometry fixture for document id 7.
|
||||
fn geometry(viewport: egui::Vec2, pixels_per_point: f32, max_texture_side: usize) -> CaptureGeometry {
|
||||
CaptureGeometry { surface: surface(viewport, pixels_per_point, max_texture_side), document: 7 }
|
||||
CaptureGeometry { surface: surface(viewport, pixels_per_point, max_texture_side), viewport: egui::ViewportId::ROOT, document: 7 }
|
||||
}
|
||||
|
||||
/// Creates a black test image of the given size.
|
||||
|
||||
+18
-11
@@ -2486,15 +2486,9 @@ impl WordApp {
|
||||
if self.ctx.is_none() {
|
||||
self.ctx = Some(ctx.clone());
|
||||
}
|
||||
// Capture geometry/document identity is current before control admission and completion.
|
||||
// The epoch is monotonic: resizing back to an earlier size is still a new epoch.
|
||||
let capture_geometry = self.surface_geometry.map(|surface| surface.with_document(self.session.document_id()));
|
||||
if let Some(geometry) = capture_geometry
|
||||
&& self.capture_geometry != Some(geometry)
|
||||
{
|
||||
self.capture_geometry = Some(geometry);
|
||||
self.capture_epoch = self.capture_epoch.wrapping_add(1);
|
||||
}
|
||||
// Capture geometry/window/document identity is current before control admission and
|
||||
// completion.
|
||||
self.refresh_capture_geometry(ctx);
|
||||
let native = self.windows.native();
|
||||
if native {
|
||||
self.printing.poll(ctx);
|
||||
@@ -2733,7 +2727,9 @@ impl WordApp {
|
||||
// Capture the surface from the raw input itself. `InputState::viewport_rect` and the
|
||||
// last painted screen rect are stale in a hidden `run_logic` tick; RawInput is the host's
|
||||
// current answer, including minimized/occluded and a Wayland `inner_rect: None`.
|
||||
if let Some(geometry) = control::SurfaceGeometry::from_raw(raw, ctx) {
|
||||
if raw.viewport_id == egui::ViewportId::ROOT
|
||||
&& let Some(geometry) = control::SurfaceGeometry::from_raw(raw, ctx)
|
||||
{
|
||||
// A hidden resize and change back still invalidate an older capture, even if
|
||||
// neither intermediate geometry reaches a drawing or logic pass.
|
||||
if self.surface_geometry != Some(geometry) {
|
||||
@@ -3101,6 +3097,17 @@ impl WordApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the capture geometry the active window's surface, viewport and document. The epoch is
|
||||
/// monotonic: resizing back to an earlier size is still a new epoch.
|
||||
pub(crate) fn refresh_capture_geometry(&mut self, ctx: &egui::Context) {
|
||||
let (viewport, surface) = self.active_surface_geometry(ctx);
|
||||
let geometry = surface.map(|surface| surface.with_target(viewport, self.session.document_id()));
|
||||
if self.capture_geometry != geometry {
|
||||
self.capture_geometry = geometry;
|
||||
self.capture_epoch = self.capture_epoch.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drains queued control requests, dispatches each through `control::handle`, sends its reply or
|
||||
/// admits a screenshot, then puts the receiver back.
|
||||
fn drain_control(&mut self, ctx: &egui::Context) {
|
||||
@@ -9753,7 +9760,7 @@ mod tests {
|
||||
let mut raw = egui::RawInput { screen_rect: Some(rect), ..Default::default() };
|
||||
raw.viewports.get_mut(&egui::ViewportId::ROOT).unwrap().native_pixels_per_point = Some(1.5);
|
||||
let surface = control::SurfaceGeometry::from_raw(&raw, &ctx).expect("screen rect fallback");
|
||||
let geometry = surface.with_document(7);
|
||||
let geometry = surface.with_target(egui::ViewportId::ROOT, 7);
|
||||
assert_eq!(geometry.expected_size(), Some([480, 300]));
|
||||
assert_eq!(geometry.expected_bytes(), Some(480 * 300 * 4));
|
||||
}
|
||||
|
||||
@@ -282,6 +282,18 @@ impl WordApp {
|
||||
self.windows.viewport(key)
|
||||
}
|
||||
|
||||
/// The active window's viewport and surface, for `ui.screenshot`: the root's from this
|
||||
/// frame's raw input, another window's from the input it last drew with (the host's raw-input
|
||||
/// hook sees only the root's).
|
||||
pub(crate) fn active_surface_geometry(&self, ctx: &egui::Context) -> (ViewportId, Option<crate::control::SurfaceGeometry>) {
|
||||
let viewport = self.active_viewport();
|
||||
if viewport == ViewportId::ROOT {
|
||||
return (viewport, self.surface_geometry);
|
||||
}
|
||||
let (info, screen_rect, max_texture_side) = ctx.input_for(viewport, |i| (i.viewport().clone(), i.raw.screen_rect, i.raw.max_texture_side));
|
||||
(viewport, crate::control::SurfaceGeometry::from_viewport(&info, screen_rect, max_texture_side, ctx))
|
||||
}
|
||||
|
||||
/// The viewport new windows start from: the host's size, icon and app id. Their title and
|
||||
/// place are the app's.
|
||||
pub fn set_window_builder(&mut self, builder: egui::ViewportBuilder) {
|
||||
@@ -1140,6 +1152,45 @@ mod tests {
|
||||
assert_eq!(text(&a), "First", "the other window got none of it");
|
||||
}
|
||||
|
||||
/// `ui.screenshot` captures the active window: the capture is sized from its viewport and the
|
||||
/// command goes to that viewport. A capture still pending when another window comes forward
|
||||
/// fails with an error instead of capturing that window.
|
||||
#[test]
|
||||
fn screenshot_captures_the_active_window() {
|
||||
let ctx = backend();
|
||||
let mut a = app("First");
|
||||
frames(&ctx, &mut a, 2);
|
||||
a.run("file.new", json!({})).unwrap();
|
||||
let child = a.active_viewport();
|
||||
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0));
|
||||
INFOS.with(|i| i.borrow_mut().entry(child).or_default().inner_rect = Some(rect));
|
||||
focus(child);
|
||||
frames(&ctx, &mut a, 4);
|
||||
|
||||
let (reply, replies) = std::sync::mpsc::channel();
|
||||
crate::control::admit_screenshot(&mut a, &ctx, None, reply);
|
||||
assert!(replies.try_recv().is_err(), "the capture is admitted");
|
||||
assert_eq!(a.shots.requests.iter().map(|s| (s.viewport, s.expected)).collect::<Vec<_>>(), [(child, [800, 600])]);
|
||||
a.shots.requests.iter_mut().for_each(|s| s.ready_at = 0.0);
|
||||
COMMANDS.with(|c| c.borrow_mut().clear());
|
||||
frames(&ctx, &mut a, 4);
|
||||
let shot_to =
|
||||
|id: ViewportId| COMMANDS.with(|c| c.borrow().get(&id).is_some_and(|v| v.iter().any(|c| matches!(c, ViewportCommand::Screenshot(_)))));
|
||||
assert!(shot_to(child), "the active window is captured");
|
||||
assert!(!shot_to(ViewportId::ROOT), "the first window is not");
|
||||
|
||||
let (reply, later) = std::sync::mpsc::channel();
|
||||
crate::control::admit_screenshot(&mut a, &ctx, None, reply);
|
||||
focus(ViewportId::ROOT);
|
||||
frames(&ctx, &mut a, 2);
|
||||
for r in [replies, later] {
|
||||
let v = r.try_recv().expect("the capture was answered");
|
||||
assert_eq!(v["ok"], false);
|
||||
assert!(v["error"].as_str().is_some_and(|e| e.contains("another window")), "{v}");
|
||||
}
|
||||
assert!(a.shots.requests.is_empty());
|
||||
}
|
||||
|
||||
/// Quitting with two documents of unsaved changes asks about each in its window, the window
|
||||
/// coming to the front; Cancel keeps both.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user