Comments: the composer hides while its comment is scrolled away (#4021)

The floating comment card (new note, text box, edit comment) was an egui
Area constrained to the whole window. When the page scrolled its comment
under the mode bar, the card was pushed back into the window and sat over
the mode bar and toolbar until the whole page left the screen.

It now shows only while its anchor is inside the document view, and is
kept inside that view. While the comment is out of sight the draft is
kept, and the editor takes the focus again when it comes back.

Test: tests/comments.rs the_composer_hides_while_its_comment_is_scrolled_away
scrolls a sticky note's anchor above the document with its page still on
screen; it fails without the change (the card sat at y=43, over the
toolbar, with the document starting at y=134) and passes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mmmaarian03-crypto
2026-10-23 08:57:40 +03:00
co-authored by Claude Opus 5.5
parent 0dc77fe4ed
commit d6ba2413ba
2 changed files with 37 additions and 1 deletions
+9 -1
View File
@@ -1564,6 +1564,7 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo,
};
let cx = PageCx { page, xf: &xf, info, tool: QuickTool::Select, prefs, allowed: true, hidden: false, redact_mark: ([0.89, 0.13, 0.13], 1.0) };
let anchor = cx.to_screen(c.at);
let viewport = view.viewport_rect();
let t = Tokens::get(ctx);
let mut post = false;
let mut cancel = false;
@@ -1576,8 +1577,15 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo,
ComposerKind::Replace => "Replacement text",
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).
if !viewport.contains(anchor) {
c.focus = true;
return None;
}
let pos = pos2(anchor.x + 12.0, anchor.y);
egui::Area::new(egui::Id::new(("comment-composer", view.id.0))).order(egui::Order::Foreground).fixed_pos(pos).show(ctx, |ui| {
let area = egui::Area::new(egui::Id::new(("comment-composer", view.id.0))).order(egui::Order::Foreground).constrain_to(viewport);
area.fixed_pos(pos).show(ctx, |ui| {
egui::Frame::popup(ui.style()).inner_margin(egui::Margin::same(12)).corner_radius(CornerRadius::same(8)).show(ui, |ui| {
ui.set_width(260.0);
ui.horizontal(|ui| {
+28
View File
@@ -675,6 +675,34 @@ fn sticky_note_composer_posts_and_returns_to_select() {
assert_eq!(comments(&h).len(), 1);
}
/// The composer leaves with its comment when the page scrolls it out of the document, and comes
/// back with the draft (#4021). It used to be pushed back into the window, over the mode bar and
/// the toolbar, while its comment was out of sight.
#[test]
fn the_composer_hides_while_its_comment_is_scrolled_away() {
let mut h = harness(|app| app.set_option("quick", "note").unwrap());
click_pt(&mut h, (250.0, 180.0));
field(&h, "Add a comment").type_text("Draft");
h.run_steps(2);
let editors = |h: &Harness<'static, PdfCraftApp>| -> Vec<egui::Rect> {
h.query_all_by(|n| n.role() == egui::accesskit::Role::MultilineTextInput).map(|n| n.rect()).collect()
};
let viewport = h.state().views[0].viewport_rect();
assert!(editors(&h).iter().any(|r| viewport.contains(r.center())), "the composer opens in the document");
// The page scrolled up: it still shows, the comment near its top doesn't.
h.state_mut().views[0].goto = Some((0, 0.4));
h.run_steps(4);
assert!(h.state().views[0].page_screen_rect(0).is_some_and(|r| r.bottom() > viewport.top()), "page 1 still shows");
assert!(at(&h, 250.0, 180.0).y < viewport.top(), "the comment is scrolled away");
let shown = editors(&h);
assert!(shown.is_empty(), "the composer stays with its comment, not over the toolbar: {shown:?} (document {viewport:?})");
// Back again, the draft is still there.
h.state_mut().views[0].goto = Some((0, 0.0));
h.run_steps(4);
assert!(editors(&h).iter().any(|r| viewport.contains(r.center())), "the composer is back");
assert_eq!(h.state().views[0].comments.composer.as_ref().map(|c| c.text.as_str()), Some("Draft"));
}
/// The rectangle of the comment whose text starts with `text`.
fn rect_of(h: &Harness<'static, PdfCraftApp>, text: &str) -> [f32; 4] {
let c = comments(h);