diff --git a/crates/ui-egui/src/panels/comments.rs b/crates/ui-egui/src/panels/comments.rs index edf0e8b84..5acb71402 100644 --- a/crates/ui-egui/src/panels/comments.rs +++ b/crates/ui-egui/src/panels/comments.rs @@ -130,29 +130,34 @@ fn comment_card(app: &mut SlideApp, ui: &mut Ui, slide: usize, index: usize, c: for r in &c.replies { ui.label(egui::RichText::new(format!("↳ {}: {}", r.author, r.text)).color(t.text_dim)); } + // The buttons are laid out first, from the right, so the reply field takes exactly the + // width they leave. A fixed allowance overflowed the card and widened the pane every + // frame until it stuck at its widest (#1431). ui.horizontal(|ui| { - let id = ui.scope_id().with(("reply", slide, index)); - let mut txt: String = ui.data_mut(|d| d.get_temp(id).unwrap_or_default()); - let r = ui.add(egui::TextEdit::singleline(&mut txt).hint_text("Reply…").desired_width(ui.available_width() - 70.0)); - if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) && !txt.trim().is_empty() { - focus_comment(app, slide, index); - run(app, "comment.reply", json!({"index": index, "text": txt})); - txt.clear(); - } - ui.data_mut(|d| d.insert_temp(id, txt)); - if ui.small_button(if c.resolved { "Reopen" } else { "Resolve" }).clicked() { - focus_comment(app, slide, index); - run(app, "comment.resolve", json!({"index": index, "resolved": !c.resolved})); - } - if ui.small_button("🗑").clicked() { - let _ = app.run("slide.go", json!({"index": slide})); - let _ = app.run("comment.delete", json!({"index": index})); - match app.ui.comment_focus { - Some((fs, fi)) if fs == slide && fi == index => app.ui.comment_focus = None, - Some((fs, fi)) if fs == slide && fi > index => app.ui.comment_focus = Some((fs, fi - 1)), - _ => {} + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.small_button("🗑").clicked() { + let _ = app.run("slide.go", json!({"index": slide})); + let _ = app.run("comment.delete", json!({"index": index})); + match app.ui.comment_focus { + Some((fs, fi)) if fs == slide && fi == index => app.ui.comment_focus = None, + Some((fs, fi)) if fs == slide && fi > index => app.ui.comment_focus = Some((fs, fi - 1)), + _ => {} + } } - } + if ui.small_button(if c.resolved { "Reopen" } else { "Resolve" }).clicked() { + focus_comment(app, slide, index); + run(app, "comment.resolve", json!({"index": index, "resolved": !c.resolved})); + } + let id = ui.scope_id().with(("reply", slide, index)); + let mut txt: String = ui.data_mut(|d| d.get_temp(id).unwrap_or_default()); + let r = ui.add(egui::TextEdit::singleline(&mut txt).hint_text("Reply…").desired_width(ui.available_width())); + if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) && !txt.trim().is_empty() { + focus_comment(app, slide, index); + run(app, "comment.reply", json!({"index": index, "text": txt})); + txt.clear(); + } + ui.data_mut(|d| d.insert_temp(id, txt)); + }); }); }); if focused { diff --git a/crates/ui-egui/src/tests.rs b/crates/ui-egui/src/tests.rs index 29fa755fd..5221544a1 100644 --- a/crates/ui-egui/src/tests.rs +++ b/crates/ui-egui/src/tests.rs @@ -2361,3 +2361,47 @@ fn overdue_visible_logic_does_not_reanchor_a_later_hidden_show() { let _ = ctx.run_logic(&raw_at(12.0, true, false), |ctx| app.tick_hidden_show(ctx, visible_anchor + std::time::Duration::from_secs(3))); assert_eq!(app.show.as_ref().unwrap().state.slide, 1, "time from the full visible frame is preserved through the logic-only pass"); } + +/// #1431: a comment card's reply row was wider than the pane, so the Comments pane grew every +/// frame until it stuck at its widest and dragging its edge did nothing. +#[test] +fn comments_pane_keeps_its_width_and_resizes() { + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SlideApp::new(Session::with_new(), Services::default()), + ); + h.run_steps(3); + h.state_mut().run("comment.add", json!({"text": "Check the figures on this slide"})).unwrap(); + h.state_mut().run("comment.add", json!({"text": "Resolved one"})).unwrap(); + h.state_mut().run("comment.resolve", json!({"index": 1, "resolved": true})).unwrap(); + h.state_mut().run("view.pane", json!({"pane": "comments"})).unwrap(); + h.run_steps(30); + let width = h.state().ui.pane_width; + assert!(width < 320.0, "the pane keeps its default width, not {width}"); + // The cards stay compact: the second one starts near the top, not below a stretched first. + let second = h.get_by_label("Resolved one").rect(); + assert!(second.top() < 450.0, "the second comment sits near the top, at {second:?}"); + + // Drag the pane's left edge: right narrows it, left widens it. + let drag = |h: &mut egui_kittest::Harness<'_, SlideApp>, dx: f32| { + let start = egui::pos2(1200.0 - h.state().ui.pane_width - 1.0, 500.0); + let end = start + egui::vec2(dx, 0.0); + h.event(egui::Event::PointerMoved(start)); + h.run_steps(1); + h.event(egui::Event::PointerButton { pos: start, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE }); + for i in 1..=6 { + h.event(egui::Event::PointerMoved(start + (end - start) * (i as f32 / 6.0))); + h.run_steps(1); + } + h.event(egui::Event::PointerButton { pos: end, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE }); + h.run_steps(10); + h.state().ui.pane_width + }; + let narrow = drag(&mut h, 50.0); + assert!(narrow < width - 40.0, "dragging right narrows the pane: {width} -> {narrow}"); + let wide = drag(&mut h, -150.0); + assert!(wide > narrow + 140.0, "dragging left widens the pane: {narrow} -> {wide}"); +}