From 0d1f3f445a688c2fbb00f98a1480af90284849fb Mon Sep 17 00:00:00 2001 From: Julian Eisel Date: Mon, 3 Jul 2023 15:15:18 +0200 Subject: [PATCH] UI: Support page based scrolling in View2D No user visible changes expected. The feature is not exposed yet. For the asset shelf (#102879), design is to use a paginated scrolling style. That means, that any scrolling will always snap to a multiple of the page size (the size used when pressing the Page Up/Down keys). Together with strict region size snapping (implemented in the asset shelf patch, #104831), this gives a clean scrolling experience where partially visible rows are avoided (impossible even). Introduces: - `View2D.flag` value `V2D_SNAP_TO_PAGESIZE_Y`, which will cause any scrolling to only use multiples of the page size. - A custom page size via `View2D.page_size_y` for when the full region size is not the appropriate page size value. - API function `UI_view2d_offset_y_snap_to_closest_page()` to enforce the snapping from outside View2D. The asset shelf uses this to keep strict scrolling over DPI changes. Pull Request: https://projects.blender.org/blender/blender/pulls/109154 --- source/blender/editors/include/UI_view2d.h | 5 ++ source/blender/editors/interface/view2d.cc | 16 ++++ .../editors/interface/view2d_intern.hh | 6 ++ .../blender/editors/interface/view2d_ops.cc | 84 +++++++++++++++---- source/blender/makesdna/DNA_view2d_types.h | 9 +- 5 files changed, 104 insertions(+), 16 deletions(-) diff --git a/source/blender/editors/include/UI_view2d.h b/source/blender/editors/include/UI_view2d.h index 42d4aeb06b3..0b3216e6404 100644 --- a/source/blender/editors/include/UI_view2d.h +++ b/source/blender/editors/include/UI_view2d.h @@ -415,6 +415,11 @@ void UI_view2d_center_set(struct View2D *v2d, float x, float y); */ void UI_view2d_offset(struct View2D *v2d, float xfac, float yfac); +/** + * Scrolls the view so that the upper edge is at a multiple of the page size. + */ +void UI_view2d_offset_y_snap_to_closest_page(struct View2D *v2d); + /** * Check if mouse is within scrollers * diff --git a/source/blender/editors/interface/view2d.cc b/source/blender/editors/interface/view2d.cc index ac16312ad32..87dbd5bf08e 100644 --- a/source/blender/editors/interface/view2d.cc +++ b/source/blender/editors/interface/view2d.cc @@ -79,6 +79,11 @@ BLI_INLINE void clamp_rctf_to_rcti(rcti *dst, const rctf *src) dst->ymax = clamp_float_to_int(src->ymax); } +float view2d_page_size_y(const View2D &v2d) +{ + return v2d.page_size_y ? v2d.page_size_y : BLI_rcti_size_y(&v2d.mask); +} + /* XXX still unresolved: scrolls hide/unhide vs region mask handling */ /* XXX there's V2D_SCROLL_HORIZONTAL_HIDE and V2D_SCROLL_HORIZONTAL_FULLR ... */ @@ -1957,6 +1962,17 @@ void UI_view2d_offset(View2D *v2d, float xfac, float yfac) UI_view2d_curRect_validate(v2d); } +void UI_view2d_offset_y_snap_to_closest_page(struct View2D *v2d) +{ + const float cur_size_y = BLI_rctf_size_y(&v2d->cur); + const float page_size_y = view2d_page_size_y(*v2d); + + v2d->cur.ymax = roundf(v2d->cur.ymax / page_size_y) * page_size_y; + v2d->cur.ymin = v2d->cur.ymax - cur_size_y; + + UI_view2d_curRect_validate(v2d); +} + char UI_view2d_mouse_in_scrollers_ex(const ARegion *region, const View2D *v2d, const int xy[2], diff --git a/source/blender/editors/interface/view2d_intern.hh b/source/blender/editors/interface/view2d_intern.hh index 629a05bcaba..4d7975d7f73 100644 --- a/source/blender/editors/interface/view2d_intern.hh +++ b/source/blender/editors/interface/view2d_intern.hh @@ -35,3 +35,9 @@ void view2d_scrollers_calc(View2D *v2d, const rcti *mask_custom, View2DScrollers void view2d_totRect_set_resize(View2D *v2d, int width, int height, bool resize); bool view2d_edge_pan_poll(bContext *C); + +/** + * For paginated scrolling, get the page height to scroll. This may be a custom height + * (#View2D.page_size_y) but defaults to the #View2D.mask height. + */ +float view2d_page_size_y(const View2D &v2d); diff --git a/source/blender/editors/interface/view2d_ops.cc b/source/blender/editors/interface/view2d_ops.cc index 69ad8865597..03c1f404fe3 100644 --- a/source/blender/editors/interface/view2d_ops.cc +++ b/source/blender/editors/interface/view2d_ops.cc @@ -46,6 +46,25 @@ static bool view2d_poll(bContext *C) return (region != nullptr) && (region->v2d.flag & V2D_IS_INIT); } +/** + * Calculate a scrolling delta that is the closest to a multiple of the page size (as returned by + * #view2d_page_size_y). So when scrolling for more than half a page size, a delta to the next page + * is returned. No scrolling change should be applied when this returns 0. + */ +static float view2d_scroll_delta_y_snap_page_size(const View2D &v2d, const float delta_y) +{ + const float page_size = view2d_page_size_y(v2d); + const int delta_pages = int((delta_y - page_size * 0.5f) / page_size); + + /* Apply no change, don't update last coordinates. */ + if (abs(delta_pages) < 1) { + return 0.0f; + } + + /* Snap the delta to a multiple of a page size. */ + return delta_pages * page_size; +} + /** \} */ /* -------------------------------------------------------------------- */ @@ -260,18 +279,32 @@ static int view_pan_invoke(bContext *C, wmOperator *op, const wmEvent *event) static int view_pan_modal(bContext *C, wmOperator *op, const wmEvent *event) { v2dViewPanData *vpd = static_cast(op->customdata); + View2D *v2d = vpd->v2d; /* execute the events */ switch (event->type) { case MOUSEMOVE: { /* calculate new delta transform, then store mouse-coordinates for next-time */ - RNA_int_set(op->ptr, "deltax", (vpd->lastx - event->xy[0])); - RNA_int_set(op->ptr, "deltay", (vpd->lasty - event->xy[1])); + int deltax = vpd->lastx - event->xy[0]; + int deltay = vpd->lasty - event->xy[1]; - vpd->lastx = event->xy[0]; - vpd->lasty = event->xy[1]; + /* Page snapping: When panning for more than half a page size, snap to the next page. */ + if (v2d->flag & V2D_SNAP_TO_PAGESIZE_Y) { + deltay = view2d_scroll_delta_y_snap_page_size(*v2d, deltay); + } - view_pan_apply(C, op); + if (deltax != 0) { + RNA_int_set(op->ptr, "deltax", deltax); + vpd->lastx = event->xy[0]; + } + if (deltay != 0) { + RNA_int_set(op->ptr, "deltay", deltay); + vpd->lasty = event->xy[1]; + } + + if (deltax || deltay) { + view_pan_apply(C, op); + } break; } /* XXX: Mode switching isn't implemented. See comments in 36818. @@ -506,9 +539,13 @@ static int view_scrolldown_exec(bContext *C, wmOperator *op) RNA_int_set(op->ptr, "deltay", -40); PropertyRNA *prop = RNA_struct_find_property(op->ptr, "page"); - if (RNA_property_is_set(op->ptr, prop) && RNA_property_boolean_get(op->ptr, prop)) { - ARegion *region = CTX_wm_region(C); - RNA_int_set(op->ptr, "deltay", region->v2d.mask.ymin - region->v2d.mask.ymax); + const bool use_page_size = (vpd->v2d->flag & V2D_SNAP_TO_PAGESIZE_Y) || + (RNA_property_is_set(op->ptr, prop) && + RNA_property_boolean_get(op->ptr, prop)); + if (use_page_size) { + const ARegion *region = CTX_wm_region(C); + const int page_size = view2d_page_size_y(region->v2d); + RNA_int_set(op->ptr, "deltay", -page_size); } /* apply movement, then we're done */ @@ -557,9 +594,13 @@ static int view_scrollup_exec(bContext *C, wmOperator *op) RNA_int_set(op->ptr, "deltay", 40); PropertyRNA *prop = RNA_struct_find_property(op->ptr, "page"); - if (RNA_property_is_set(op->ptr, prop) && RNA_property_boolean_get(op->ptr, prop)) { - ARegion *region = CTX_wm_region(C); - RNA_int_set(op->ptr, "deltay", BLI_rcti_size_y(®ion->v2d.mask)); + const bool use_page_size = (vpd->v2d->flag & V2D_SNAP_TO_PAGESIZE_Y) || + (RNA_property_is_set(op->ptr, prop) && + RNA_property_boolean_get(op->ptr, prop)); + if (use_page_size) { + const ARegion *region = CTX_wm_region(C); + const int page_size = view2d_page_size_y(region->v2d); + RNA_int_set(op->ptr, "deltay", page_size); } /* apply movement, then we're done */ @@ -2000,21 +2041,24 @@ static void scroller_activate_apply(bContext *C, wmOperator *op) static int scroller_activate_modal(bContext *C, wmOperator *op, const wmEvent *event) { v2dScrollerMove *vsm = static_cast(op->customdata); + const bool use_page_size_y = vsm->v2d->flag & V2D_SNAP_TO_PAGESIZE_Y; /* execute the events */ switch (event->type) { case MOUSEMOVE: { + float delta = 0.0f; + /* calculate new delta transform, then store mouse-coordinates for next-time */ if (ELEM(vsm->zone, SCROLLHANDLE_BAR, SCROLLHANDLE_MAX)) { /* if using bar (i.e. 'panning') or 'max' zoom widget */ switch (vsm->scroller) { case 'h': /* horizontal scroller - so only horizontal movement * ('cur' moves opposite to mouse) */ - vsm->delta = float(event->xy[0] - vsm->lastx); + delta = float(event->xy[0] - vsm->lastx); break; case 'v': /* vertical scroller - so only vertical movement * ('cur' moves opposite to mouse) */ - vsm->delta = float(event->xy[1] - vsm->lasty); + delta = float(event->xy[1] - vsm->lasty); break; } } @@ -2023,15 +2067,25 @@ static int scroller_activate_modal(bContext *C, wmOperator *op, const wmEvent *e switch (vsm->scroller) { case 'h': /* horizontal scroller - so only horizontal movement * ('cur' moves with mouse) */ - vsm->delta = float(vsm->lastx - event->xy[0]); + delta = float(vsm->lastx - event->xy[0]); break; case 'v': /* vertical scroller - so only vertical movement * ('cur' moves with to mouse) */ - vsm->delta = float(vsm->lasty - event->xy[1]); + delta = float(vsm->lasty - event->xy[1]); break; } } + /* Page snapping: When panning for more than half a page size, snap to the next page. */ + if (use_page_size_y && (vsm->scroller == 'v')) { + delta = view2d_scroll_delta_y_snap_page_size(*vsm->v2d, delta * vsm->fac) / vsm->fac; + } + + if (IS_EQF(delta, 0.0f)) { + break; + } + + vsm->delta = delta; /* store previous coordinates */ vsm->lastx = event->xy[0]; vsm->lasty = event->xy[1]; diff --git a/source/blender/makesdna/DNA_view2d_types.h b/source/blender/makesdna/DNA_view2d_types.h index b6185b1c946..d286df8a6ee 100644 --- a/source/blender/makesdna/DNA_view2d_types.h +++ b/source/blender/makesdna/DNA_view2d_types.h @@ -61,7 +61,11 @@ typedef struct View2D { /* Usually set externally (as in, not in view2d files). */ /** Alpha of vertical and horizontal scroll-bars (range is [0, 255]). */ char alpha_vert, alpha_hor; - char _pad[6]; + + char _pad[2]; + /** When set (not 0), determines how many pixels to scroll when scrolling an entire page. + * Otherwise the height of #View2D.mask is used. */ + float page_size_y; /* animated smooth view */ struct SmoothView2DStore *sms; @@ -123,6 +127,9 @@ enum { V2D_IS_NAVIGATING = (1 << 9), /* view settings need to be set still... */ V2D_IS_INIT = (1 << 10), + /* Ensure scrolling always snaps to multiples of #View2D.page_size_y or the #View2D.mask height + * if this is 0. Zooming doesn't respect this. */ + V2D_SNAP_TO_PAGESIZE_Y = (1 << 11), }; /** Scroller flags for View2D (#View2D.scroll). */