fix(ui): scrolling the Paths or Channels list no longer moves the panel's footer (part of #6238)

The dock lists the tabs that lay out their own scrolling list and footer so it
does not wrap them in a scroll area of its own (Layers, History, Navigator).
Channels and Paths do exactly the same, but were left off, so each sat inside
the dock's scroll area as well as its own, and the two nested areas shared the
mouse wheel. At 125 % display scaling, for some window heights, the outer
content overflowed by one physical pixel and scrolled with the list: the footer's
buttons moved about a pixel while scrolling down and back while scrolling up.

Group::scrolls_itself now covers all three tabs of the Layers group.

Measured: with 30 paths, the footer moved at 31 of 723 window-height and scale
combinations (all at 125 %); with the change, none. The reporter's video agrees:
the footer strip shifts 1 px and back repeatedly while the tab strip never moves.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ArchersIntel
2026-10-16 02:25:01 -05:00
co-authored by Claude Sonnet 5.5
parent d27546970f
commit 8f5d53027c
3 changed files with 76 additions and 2 deletions
+5 -2
View File
@@ -120,9 +120,12 @@ impl Group {
}
/// Tabs that lay out their own scrolling list and footer, or size themselves to the group
/// (the Navigator's preview grows with it, as in Photoshop: #2726).
/// (the Navigator's preview grows with it, as in Photoshop: #2726). Channels and Paths are
/// Layers' siblings and do the same: wrapped in the dock's scroll area as well, the two nested
/// areas shared the wheel, and where the outer content overflowed by one physical pixel (at
/// 125 % scaling for some window heights) the footer and list moved a pixel as you scrolled (#6238).
pub fn scrolls_itself(self, tab: usize) -> bool {
matches!((self, tab), (Group::Layers, 0) | (Group::History, 0) | (Group::History, 1) | (Group::Navigator, 0))
matches!((self, tab), (Group::Layers, 0..=2) | (Group::History, 0) | (Group::History, 1) | (Group::Navigator, 0))
}
pub(crate) fn tab_mut(self, tabs: &mut DockTabs) -> &mut usize {
+2
View File
@@ -2732,6 +2732,8 @@ mod warp_text_dialog_tests;
#[cfg(test)]
mod blend_dropdown_wheel_tests;
#[cfg(test)]
mod paths_scroll_tests;
#[cfg(test)]
mod options_bar_mode_wheel_tests;
+69
View File
@@ -0,0 +1,69 @@
//! Scrolling the Paths panel moves only its list (#6238).
use egui::vec2;
use egui_kittest::{Harness, kittest::Queryable};
use serde_json::json;
use crate::PhotocraftApp;
fn harness(paths: usize, size: egui::Vec2, ppp: f32) -> Harness<'static, PhotocraftApp> {
let mut h = Harness::builder().with_size(size).with_pixels_per_point(ppp).with_max_steps(64).build_eframe(move |cc| {
PhotocraftApp::setup_context(&cc.egui_ctx, Default::default());
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
for i in 0..paths {
let x = (i % 8) as i32 * 40;
app.run("select.rect", json!({"x": x, "y": 20, "width": 30, "height": 30})).unwrap();
app.run("select.toWorkPath", json!({"tolerance": 2})).unwrap();
app.run("path.rename", json!({"name": "work", "to": format!("Path {}", i + 1)})).unwrap();
}
// Layers | Channels | Paths: the Paths tab.
app.ui.dock_tabs.layers = 2;
app
});
h.run_steps(8);
h
}
/// Scrolling the path list moves the rows and nothing else: the footer's buttons stay where they
/// are. The Paths panel lays out its own list and footer, but the dock also wrapped it in a
/// scroll area; at 125 % scaling and some window heights that outer area overflowed by one
/// physical pixel, took part of the wheel and moved the footer about a pixel down and back up.
#[test]
fn scrolling_the_paths_list_does_not_move_the_footer() {
// Heights that moved it at 125 % (two bands of window heights), and ones that never did.
for (ppp, height) in [(1.25, 520.0), (1.25, 900.0), (1.25, 960.0), (1.0, 900.0), (1.5, 900.0)] {
let mut h = harness(30, vec2(1440.0, height), ppp);
let footer = |h: &Harness<'_, PhotocraftApp>| h.query_by_label("Delete path").map(|r| r.rect().min.y);
let rest = footer(&h).unwrap_or_else(|| panic!("the footer shows at {ppp}x, {height} high"));
let first_row = h.query_by_label("Path 1").unwrap().rect();
h.hover_at(first_row.center());
h.run_steps(2);
for i in 0..40 {
// Down to the end of the list and past it, then back up.
let dir = if i < 30 { -1.0 } else { 1.0 };
h.event(egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Line,
delta: vec2(0.0, dir),
phase: egui::TouchPhase::Move,
modifiers: Default::default(),
});
h.run_steps(1);
assert_eq!(footer(&h), Some(rest), "{ppp}x, {height} high, notch {i}: the footer stayed put");
}
// The list itself did scroll (the first row is out of view or has moved).
let moved = h.query_by_label("Path 1").map(|r| r.rect().min.y) != Some(first_row.min.y);
assert!(moved || h.query_by_label("Path 1").is_none(), "{ppp}x, {height} high: the list scrolled");
}
}
/// Channels, Paths and Layers are siblings with the same own-scroll layout, so none is wrapped in
/// the dock's scroll area.
#[test]
fn the_layers_group_tabs_all_scroll_themselves() {
use crate::dock::Group;
for tab in 0..=2 {
assert!(Group::Layers.scrolls_itself(tab), "Layers group tab {tab}");
}
assert!(!Group::Layers.scrolls_itself(3), "a tab that does not exist is not special");
}