diff --git a/crates/ui-egui/src/comments.rs b/crates/ui-egui/src/comments.rs index 2d2571863..0e2db115f 100644 --- a/crates/ui-egui/src/comments.rs +++ b/crates/ui-egui/src/comments.rs @@ -1593,10 +1593,23 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, ComposerKind::Edit(_) => "Edit comment", }; // Its comment scrolled out of the document: keep the draft until it is back, rather than - // pushing the card into the window over the mode bar and toolbar (#4021). + // pushing the card into the window over the mode bar and toolbar (#4021). An edited comment + // that is only partly scrolled away keeps its card, at the nearest point inside the document. + let mut anchor = anchor; if !viewport.contains(anchor) { - c.focus = true; - return None; + let part_visible = matches!(c.kind, ComposerKind::Edit(index) + if info.annotations.iter().any(|a| a.page == page && a.index == index && { + let r = a.rect; + viewport.intersects(egui::Rect::from_two_pos( + cx.to_screen([f64::from(r[0]), f64::from(r[1])]), + cx.to_screen([f64::from(r[2]), f64::from(r[3])]), + )) + })); + if !part_visible { + c.focus = true; + return None; + } + anchor = anchor.clamp(viewport.min, viewport.max); } let pos = pos2(anchor.x + 12.0, anchor.y); let area = egui::Area::new(egui::Id::new(("comment-composer", view.id.0))).order(egui::Order::Foreground).constrain_to(viewport); diff --git a/crates/ui-egui/tests/it/comments.rs b/crates/ui-egui/tests/it/comments.rs index 9f142c8f7..a273e3cd6 100644 --- a/crates/ui-egui/tests/it/comments.rs +++ b/crates/ui-egui/tests/it/comments.rs @@ -725,6 +725,33 @@ fn the_composer_hides_while_its_comment_is_scrolled_away() { assert_eq!(h.state().views[0].comments.composer.as_ref().map(|c| c.text.as_str()), Some("Draft")); } +/// Editing a large comment whose top-right corner is scrolled away still shows the composer +/// while the rest of the comment is on screen, so it can be posted or cancelled (#4095). +#[test] +fn the_edit_composer_shows_while_part_of_its_comment_is_on_screen() { + let mut h = harness(|app| app.set_option("quick", "square").unwrap()); + drag_pt(&mut h, (20.0, 190.0), (280.0, 10.0)); + // What "Edit text…" opens: the composer anchored at the comment's top-right corner. + h.state_mut().views[0].comments.composer = Some(pdfcraft_ui_egui::comments::Composer { + page: 0, + at: [280.0, 190.0], + kind: pdfcraft_ui_egui::comments::ComposerKind::Edit(0), + text: String::new(), + focus: true, + }); + h.run_steps(2); + let viewport = h.state().views[0].viewport_rect(); + h.state_mut().views[0].goto = Some((0, 0.4)); + h.run_steps(4); + assert!(at(&h, 280.0, 190.0).y < viewport.top(), "the comment's top-right corner is scrolled away"); + assert!(at(&h, 280.0, 10.0).y > viewport.top(), "part of the comment is still on screen"); + let editors: Vec<_> = h.query_all_by(|n| n.role() == egui::accesskit::Role::MultilineTextInput).map(|n| n.rect()).collect(); + assert!(editors.iter().any(|r| viewport.contains(r.center())), "the editor is drawn: {editors:?} (document {viewport:?})"); + h.get_by_label("Cancel").click(); + h.run_steps(2); + assert!(h.state().views[0].comments.composer.is_none(), "Cancel closes it"); +} + /// With its whole page scrolled off the screen the composer keeps its draft, and the editor takes /// the keyboard again when the page comes back (#4021). #[test]