From cd9e30c3b0d5de8c502e9717f7f92f59abb015e0 Mon Sep 17 00:00:00 2001 From: Valentin Klein Date: Thu, 15 Oct 2026 08:53:00 +0200 Subject: [PATCH] Tabs: keep the drop line in front of the first tab inside the strip (#2225) (#2278) The line is drawn half a gap left of the first tab, which is outside the scroll area's clip rect, so it was clipped away. Clamp it into the clip rect. Co-authored-by: Claude Sonnet 5.5 --- crates/ui-egui/src/chrome.rs | 4 ++++ crates/ui-egui/tests/ui.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 25faedc168..e8bf3b48dd 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -191,6 +191,10 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) { ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing); if let Some(marker) = drop_marker(&slots, from, to, ui.spacing().item_spacing.x) { let span = slots.first().map_or(ui.max_rect().y_range(), |(_, r)| r.y_range()); + // In front of the first tab the gap lies outside the scroll area's + // clip rect (#2225): keep the whole line inside it. + let clip = ui.clip_rect(); + let marker = marker.clamp(clip.left() + 1.0, (clip.right() - 1.0).max(clip.left() + 1.0)); ui.painter().vline(marker, span.shrink(4.0), Stroke::new(2.0, t.accent)); } } diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index a40ac422c8..1b6276d7c4 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -1285,6 +1285,42 @@ fn dragging_a_tab_moves_it_and_keeps_the_same_document_active() { } } +/// Regression (#2225): the drop line in front of the first tab sat half a gap outside the +/// scroll area's clip rect and was never visible. +#[test] +fn the_drop_line_shows_in_front_of_the_first_tab() { + use egui::accesskit::Role; + let mut h = three_tab_harness(); + let first = h.get_by_role_and_label(Role::Tab, "first.pdf").rect(); + // Pixels in the strip just left of the first tab that are strongly blue (the accent line). + let blue_left_of = |h: &mut Harness<'static, PdfCraftApp>, rect: egui::Rect| { + let image = h.render().unwrap(); + let scale = image.width() as f32 / h.ctx.content_rect().width(); + let x0 = ((rect.left() - 3.0) * scale).max(0.0) as u32; + let x1 = ((rect.left() + 1.0) * scale).max(0.0) as u32; + let (y0, y1) = ((rect.top() * scale) as u32, (rect.bottom() * scale) as u32); + let mut n = 0; + for y in y0..y1.min(image.height()) { + for x in x0..x1.min(image.width()) { + let p = image.get_pixel(x, y).0; + if p[2] > 150 && p[0] < 90 && p[1] < 140 { + n += 1; + } + } + } + n + }; + let before = blue_left_of(&mut h, first); + // last.pdf to the left of the first tab, released not yet. + let to = first.left_center() - egui::vec2(10.0, 0.0); + drag_named_tab(&mut h, "last.pdf", to, false); + let during = blue_left_of(&mut h, first); + assert!(during > before + 10, "no drop line left of the first tab ({before} -> {during})"); + if let Ok(dir) = std::env::var("PDFCRAFT_TAB_SHOTS") { + h.render().unwrap().save(format!("{dir}/dragging-to-front.png")).unwrap(); + } +} + #[test] fn a_tab_dropped_where_it_was_stays_and_a_click_still_selects_it() { let mut h = three_tab_harness();