Merge pull request #7594 from dotCooCoo/fix/panel-autoscroll-short-list

Panels: drag autoscroll scrolls the right way in a short list
This commit is contained in:
BFlat
2026-11-14 21:54:31 +07:00
committed by GitHub
2 changed files with 40 additions and 4 deletions
+39 -3
View File
@@ -529,6 +529,21 @@ const DRAG_SCROLL_EDGE: f32 = 28.0;
/// the list's edge.
const DRAG_SCROLL_SPEED: (f32, f32) = (60.0, 280.0);
/// The scrolling [`scroll_at_edges`] asks for, in points per second (positive shows earlier rows),
/// with the pointer `depth` points below the top of a list view `view_height` points tall. Each
/// band is [`DRAG_SCROLL_EDGE`] deep, or half the view in a view shorter than two bands, so the
/// pointer is in one band at most and the middle of a short view scrolls neither way. A view with
/// no height, or a value that isn't a number, gives 0.
fn drag_scroll_speed(view_height: f32, depth: f32) -> f32 {
if !(view_height.is_finite() && view_height > 0.0) {
return 0.0;
}
let band = DRAG_SCROLL_EDGE.min(view_height / 2.0);
let (slow, more) = DRAG_SCROLL_SPEED;
let speed = |depth: f32| if (0.0..band).contains(&depth) { slow + more * (1.0 - depth / band) } else { 0.0 };
speed(depth) - speed(view_height - depth)
}
/// While something is dragged near the top or bottom edge of a scrolled list (`ui`: its contents),
/// scroll that way, faster nearer the edge, and draw the next frame so a pointer held still keeps
/// it scrolling. The pointer must be over the list's width. Each frame scrolls by the time it
@@ -540,9 +555,7 @@ pub(crate) fn scroll_at_edges(ui: &mut Ui) {
if !view.x_range().contains(pos.x) {
return;
}
let (slow, more) = DRAG_SCROLL_SPEED;
let speed = |depth: f32| if (0.0..DRAG_SCROLL_EDGE).contains(&depth) { slow + more * (1.0 - depth / DRAG_SCROLL_EDGE) } else { 0.0 };
let per_second = speed(pos.y - view.top()) - speed(view.bottom() - pos.y);
let per_second = drag_scroll_speed(view.height(), pos.y - view.top());
if per_second != 0.0 {
let dt = ui.input(|i| i.stable_dt).min(0.1);
ui.scroll_with_delta_animation(vec2(0.0, per_second * dt), egui::style::ScrollAnimation::none());
@@ -683,6 +696,29 @@ mod tests {
let tint = Paint::Solid { color: Color::gray(0.5), swatch: Some("Ink".into()), tint: 0.5 };
assert_eq!(paint_params(&tint), json!({"swatch": "Ink", "tint": 50.0}), "a tint keeps its percentage");
}
#[test]
fn drag_scrolling_in_a_short_list_goes_toward_the_nearer_edge() {
let near = |a: f32, b: f32| (a - b).abs() < 1e-3;
// A 40 pt view: each band is 20 pt deep, so the pointer is in one band at most.
for (depth, want) in [(5.0, 270.0), (15.0, 130.0), (20.0, 0.0), (25.0, -130.0), (35.0, -270.0)] {
let got = drag_scroll_speed(40.0, depth);
assert!(near(got, want), "40 pt view, pointer {depth} pt down: {got} pt/s, want {want}");
}
// Just outside a 20 pt view, the pointer does not scroll the list toward the far edge.
for depth in [-1.0, 21.0] {
assert_eq!(drag_scroll_speed(20.0, depth), 0.0, "20 pt view, pointer {depth} pt down");
}
// From 56 pt up, the bands stay 28 pt deep and the speeds are unchanged.
for (h, depth, want) in [(100.0, 5.0, 290.0), (100.0, 95.0, -290.0), (100.0, 50.0, 0.0), (56.0, 0.0, 340.0), (56.0, 27.0, 70.0)] {
let got = drag_scroll_speed(h, depth);
assert!(near(got, want), "{h} pt view, pointer {depth} pt down: {got} pt/s, want {want}");
}
for h in [0.0, -10.0, f32::NAN, f32::INFINITY] {
assert_eq!(drag_scroll_speed(h, 5.0), 0.0, "view height {h}");
}
assert_eq!(drag_scroll_speed(40.0, f32::NAN), 0.0);
}
}
#[cfg(test)]
mod tests_appearance;
+1 -1
View File
@@ -884,7 +884,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Screen readers get the value of the color and channel sliders that panels and dialogs draw (the Color panel, the Color Picker, Swatch Options, the Navigator's Zoom, the Character panel's variable font axes, the Perspective Grid and flattener options, and the sliders of the effect, tool option and transform dialogs): the number and text of the field beside each one, such as 128 for G or 50% for an opacity, with the slider's range, which extends to include a number past the slider's end. The harmony wheel's Brightness slider, which has no field, reports its brightness in percent.
- A layer whose Layer Options › Print is off shows its name italic in the Layers panel, and while its row is dragged, as the reference application shows non-printing layers (#5739); a printing layer stays upright and ticking Print again restores the upright name (`panels::layers` test `a_layer_that_does_not_print_has_its_name_italic`).
- In the Layers panel only the primary mouse button drags. A drag with the right or middle button on a row, a guide row, the selection square, the eye or lock column or a target circle moves, copies, hides, locks and transfers nothing and records no undo step, and a right-click still opens the row's menu (`panels::layers` test `only_the_primary_button_drags_rows_guides_art_columns_and_appearances`).
- Rows dragged in the Layers panel and held within 28 points of the list's top or bottom edge scroll a long list that way, faster nearer the edge (60 points a second at the band's inner side, 340 at the edge, at every frame rate), and keep scrolling while the pointer stays still, so a row out of view can take the drop (a move, or a copy with Alt); the Libraries panel's list scrolls by the same rule. A pointer beside the list, a drag of the eye or lock column and a release stop the scrolling, and scrolling changes nothing in the document. The band and speeds are VectorCraft's own (`panels::layers` tests `rows_held_near_the_bottom_or_top_edge_scroll_the_list_to_a_row_out_of_view`, `a_held_row_scrolls_the_list_as_far_in_the_same_time_at_any_frame_rate` and `the_list_scrolls_only_for_rows_held_over_it`).
- Rows dragged in the Layers panel and held within 28 points of the list's top or bottom edge scroll a long list that way, faster nearer the edge (60 points a second at the band's inner side, 340 at the edge, at every frame rate), and keep scrolling while the pointer stays still, so a row out of view can take the drop (a move, or a copy with Alt); the Libraries panel's list scrolls by the same rule. In a list view shorter than 56 points each band is half the view, so a pointer in the top half scrolls up, one in the bottom half scrolls down, and a pointer just outside the view does not scroll it toward the far edge. A pointer beside the list, a drag of the eye or lock column and a release stop the scrolling, and scrolling changes nothing in the document. The band and speeds are VectorCraft's own (`panels::layers` tests `rows_held_near_the_bottom_or_top_edge_scroll_the_list_to_a_row_out_of_view`, `a_held_row_scrolls_the_list_as_far_in_the_same_time_at_any_frame_rate` and `the_list_scrolls_only_for_rows_held_over_it`; `panels` test `drag_scrolling_in_a_short_list_goes_toward_the_nearer_edge`).
- The other panels' item drags start only with the primary mouse button too: swatches and color groups, brushes, symbols, graphic styles, library items and groups, character and paragraph styles, Appearance rows and its thumbnail, the Fill and Stroke squares, Recent Colors and the Gradient panel's thumbnail. A right or middle drag on one carries nothing, moves, copies, deletes and applies nothing and records no undo step; right-click menus and the Graphic Styles panel's held right-button preview stay as they were (`panels::swatches` test `only_a_primary_drag_moves_swatches_and_groups`, `panels::text_styles` test `only_a_primary_drag_carries_a_style_row`, `panels::tests_symbol_libraries` test `only_a_primary_drag_carries_a_library_symbol_out`, `panels::tests_effectedit` test `only_a_primary_drag_carries_the_thumbnail_or_a_row`).
- Panels undock (#495): a dock tab, a panel icon or a popped-out panel's title dragged out of the dock floats that panel (in an OS window of its own on the desktop, #4135), and the strip right of the dock's tabs floats the whole tabbed group. A floating group moves by its title bar, a tab dragged out of it floats on its own, and dropped on another group's title bar or tabs it stacks with that group; dropped on a dock group's tabs, which light up under the pointer, its panels join that group, and on the group's upper or lower half they make a new group above or below it (`window.panel.dock {anchor, zone}`); dropped on the icon column they become icons there, the tabbed group's panels too (`window.panel.dock {icon: true}`); dropped anywhere else on the dock, which lights up, they go back where they live (the tabbed group or the icon column), as its × and the panel menus' Float Panel / Dock Panel do (`window.panel.float`, `window.panel.dock`). The Tools panel floats by its title bar and docks again on the window's left edge. Floating groups stack into sets: dropped on the upper or lower half of another group (a line shows where), a group joins it right above or below it, and the set's groups stack top to bottom, each showing one panel, moving together by the top group's title bar, whose × docks them all; a group dragged by its tab strip leaves the set (`window.panel.float {below, above}`). A double-click on a tab collapses a floating group to its tabs (`collapsed`), and a dock group too, a group below it moving up into the room; a second double-click, or a click on another of its tabs, expands it (`window.panel.collapse`, kept with the preferences and in saved workspaces). Sets dropped on the left edge of the icon column or of another dock column dock as dock columns (a multi-column dock, `window.panel.dock {column}`), whose groups stack, tab and tear out as a floating set's do, and whose top strip floats the column again. Floating panels, their sets and the Tools panel's position are kept with the preferences and in user workspaces, and clamped into a smaller window. Not done: panels in separate OS windows, reordering tabs by dragging, collapsing a set or dock column to icons, dock columns right of the default dock, resizing floating panels and the groups of a set.
- A floating panel's OS window hides while a tool group's flyout or the All Tools drawer is open in the main window, as it does under a modal dialog or a menu, and shows again when the flyout or the drawer closes (#5813; `a_tool_flyout_hides_the_palette_windows_until_it_closes`).