[gnome-shell/wip/carlosg/appgrid-navigation: 17/24] js/appDisplay: Implement navigation of pages by hovering/clicking edges




commit d75ed55ed8de8c046ea8c491bd6472bd51813934
Author: Carlos Garnacho <carlosg gnome org>
Date:   Wed Feb 3 12:46:07 2021 +0100

    js/appDisplay: Implement navigation of pages by hovering/clicking edges
    
    Add the necessary animations to slide in the icons in the previous/next
    pages, also needing to 1) drop the viewport clipping, and 2) extend scrollview
    fade effects to let see the pages in the navigated direction(s).
    
    The animation is driven via 2 adjustments, one for each side, so they
    can animate independently.
    
    Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1630>

 data/theme/gnome-shell-sass/widgets/_app-grid.scss |  14 ++
 js/ui/appDisplay.js                                | 239 ++++++++++++++++++++-
 2 files changed, 252 insertions(+), 1 deletion(-)
---
diff --git a/data/theme/gnome-shell-sass/widgets/_app-grid.scss 
b/data/theme/gnome-shell-sass/widgets/_app-grid.scss
index 0e3c54c92e..5620ac26ff 100644
--- a/data/theme/gnome-shell-sass/widgets/_app-grid.scss
+++ b/data/theme/gnome-shell-sass/widgets/_app-grid.scss
@@ -136,3 +136,17 @@ $app_grid_fg_color: #fff;
   border-radius: 99px;
   icon-size: $app_icon_size * 0.5;
 }
+
+.page-navigation-hint {
+  background: rgba(255, 255, 255, 0.05);
+  width: 88px;
+
+  &.next {
+    &:ltr { border-radius: 15px 0px 0px 15px; }
+    &:rtl { border-radius: 0px 15px 15px 0px; }
+  }
+  &.previous {
+    &:ltr { border-radius: 0px 15px 15px 0px; }
+    &:rtl { border-radius: 15px 0px 0px 15px; }
+  }
+}
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index d4a627fcb5..ad791b26a8 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -38,6 +38,10 @@ var APP_ICON_TITLE_COLLAPSE_TIME = 100;
 
 const FOLDER_DIALOG_ANIMATION_TIME = 200;
 
+const PAGE_PREVIEW_ANIMATION_TIME = 150;
+const PAGE_PREVIEW_FADE_EFFECT_OFFSET = 160;
+const PAGE_INDICATOR_FADE_TIME = 200;
+
 const OVERSHOOT_THRESHOLD = 20;
 const OVERSHOOT_TIMEOUT = 1000;
 
@@ -48,6 +52,12 @@ const DIALOG_SHADE_HIGHLIGHT = Clutter.Color.from_pixel(0x00000055);
 
 let discreteGpuAvailable = false;
 
+var SidePages = {
+    NONE: 0,
+    PREVIOUS: 1 << 0,
+    NEXT: 1 << 1,
+};
+
 function _getCategories(info) {
     let categoriesStr = info.get_categories();
     if (!categoriesStr)
@@ -149,6 +159,10 @@ var BaseAppView = GObject.registerClass({
         this._canScroll = true; // limiting scrolling speed
         this._scrollTimeoutId = 0;
         this._scrollView.connect('scroll-event', this._onScroll.bind(this));
+        this._scrollView.connect('motion-event', this._onMotion.bind(this));
+        this._scrollView.connect('enter-event', this._onMotion.bind(this));
+        this._scrollView.connect('leave-event', this._onLeave.bind(this));
+        this._scrollView.connect('button-press-event', this._onButtonPress.bind(this));
 
         this._scrollView.add_actor(this._grid);
 
@@ -172,12 +186,44 @@ var BaseAppView = GObject.registerClass({
             this._scrollView.event(event, false);
         });
 
+        // Navigation indicators
+        this._nextPageIndicator = new St.Widget({
+            style_class: 'page-navigation-hint next',
+            opacity: 0,
+            visible: false,
+            reactive: false,
+            x_expand: true,
+            y_expand: true,
+            x_align: Clutter.ActorAlign.END,
+            y_align: Clutter.ActorAlign.FILL,
+        });
+
+        this._prevPageIndicator = new St.Widget({
+            style_class: 'page-navigation-hint previous',
+            opacity: 0,
+            visible: false,
+            reactive: false,
+            x_expand: true,
+            y_expand: true,
+            x_align: Clutter.ActorAlign.START,
+            y_align: Clutter.ActorAlign.FILL,
+        });
+
+        const scrollContainer = new St.Widget({
+            layout_manager: new Clutter.BinLayout(),
+            clip_to_allocation: true,
+            y_expand: true,
+        });
+        scrollContainer.add_child(this._prevPageIndicator);
+        scrollContainer.add_child(this._nextPageIndicator);
+        scrollContainer.add_child(this._scrollView);
+
         this._box = new St.BoxLayout({
             vertical: true,
             x_expand: true,
             y_expand: true,
         });
-        this._box.add_child(this._scrollView);
+        this._box.add_child(scrollContainer);
         this._box.add_child(this._pageIndicators);
 
         // Swipe
@@ -221,6 +267,8 @@ var BaseAppView = GObject.registerClass({
         this._dragCancelledId = 0;
 
         this.connect('destroy', this._onDestroy.bind(this));
+
+        this._previewedPages = new Map();
     }
 
     _onDestroy() {
@@ -243,9 +291,21 @@ var BaseAppView = GObject.registerClass({
         this._disconnectDnD();
     }
 
+    _updateFadeForNavigation() {
+        const fadeMargin = new Clutter.Margin();
+        fadeMargin.right = (this._pagesShown & SidePages.NEXT) !== 0
+            ? -PAGE_PREVIEW_FADE_EFFECT_OFFSET : 0;
+        fadeMargin.left = (this._pagesShown & SidePages.PREVIOUS) !== 0
+            ? -PAGE_PREVIEW_FADE_EFFECT_OFFSET : 0;
+        this._scrollView.update_fade_effect(fadeMargin);
+    }
+
     _updateFade() {
         const { pagePadding } = this._grid.layout_manager;
 
+        if (this._pagesShown)
+            return;
+
         if (pagePadding.top === 0 &&
             pagePadding.right === 0 &&
             pagePadding.bottom === 0 &&
@@ -327,6 +387,41 @@ var BaseAppView = GObject.registerClass({
         return Clutter.EVENT_STOP;
     }
 
+    _pageForCoords(x, y) {
+        const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
+        const { allocation } = this._grid;
+
+        const [success, pointerX] = this._scrollView.transform_stage_point(x, y);
+        if (!success)
+            return SidePages.NONE;
+
+        if (pointerX < allocation.x1)
+            return rtl ? SidePages.NEXT : SidePages.PREVIOUS;
+        else if (pointerX > allocation.x2)
+            return rtl ? SidePages.PREVIOUS : SidePages.NEXT;
+
+        return SidePages.NONE;
+    }
+
+    _onMotion(actor, event) {
+        const page = this._pageForCoords(...event.get_coords());
+        this._slideSidePages(page);
+
+        return Clutter.EVENT_PROPAGATE;
+    }
+
+    _onButtonPress(actor, event) {
+        const page = this._pageForCoords(...event.get_coords());
+        if (page === SidePages.NEXT)
+            this.goToPage(this._grid.currentPage + 1);
+        else if (page === SidePages.PREVIOUS)
+            this.goToPage(this._grid.currentPage - 1);
+    }
+
+    _onLeave() {
+        this._slideSidePages(SidePages.NONE);
+    }
+
     _swipeBegin(tracker, monitor) {
         if (monitor !== Main.layoutManager.primaryIndex)
             return;
@@ -351,6 +446,8 @@ var BaseAppView = GObject.registerClass({
         const adjustment = this._adjustment;
         const value = endProgress * adjustment.page_size;
 
+        this._syncPageHints(endProgress);
+
         adjustment.ease(value, {
             mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
             duration,
@@ -868,12 +965,37 @@ var BaseAppView = GObject.registerClass({
         this._grid.ease(params);
     }
 
+    _syncPageHints(pageNumber, animate = true) {
+        const showingNextPage = this._pagesShown & SidePages.NEXT;
+        const showingPrevPage = this._pagesShown & SidePages.PREVIOUS;
+        const duration = animate ? PAGE_INDICATOR_FADE_TIME : 0;
+
+        if (showingPrevPage) {
+            const opacity = pageNumber === 0 ? 0 : 255;
+            this._prevPageIndicator.visible = true;
+            this._prevPageIndicator.ease({
+                opacity,
+                duration,
+            });
+        }
+
+        if (showingNextPage) {
+            const opacity = pageNumber === this._grid.nPages - 1 ? 0 : 255;
+            this._nextPageIndicator.visible = true;
+            this._nextPageIndicator.ease({
+                opacity,
+                duration,
+            });
+        }
+    }
+
     goToPage(pageNumber, animate = true) {
         pageNumber = Math.clamp(pageNumber, 0, this._grid.nPages - 1);
 
         if (this._grid.currentPage === pageNumber)
             return;
 
+        this._syncPageHints(pageNumber, animate);
         this._grid.goToPage(pageNumber, animate);
     }
 
@@ -894,6 +1016,121 @@ var BaseAppView = GObject.registerClass({
         this._availWidth = availWidth;
         this._availHeight = availHeight;
     }
+
+    _getPagePreviewAdjustment(page) {
+        const previewedPage = this._previewedPages.get(page);
+        return previewedPage?.adjustment;
+    }
+
+    _syncClip() {
+        const nextPageAdjustment = this._getPagePreviewAdjustment(1);
+        const prevPageAdjustment = this._getPagePreviewAdjustment(-1);
+        this._grid.clip_to_view =
+            (!prevPageAdjustment || prevPageAdjustment.value === 0) &&
+            (!nextPageAdjustment || nextPageAdjustment.value === 0);
+    }
+
+    _setupPagePreview(page, state) {
+        if (this._previewedPages.has(page))
+            return this._previewedPages.get(page).adjustment;
+
+        const adjustment = new St.Adjustment({
+            actor: this,
+            lower: 0,
+            upper: 1,
+        });
+
+        const indicator = page > 0
+            ? this._nextPageIndicator : this._prevPageIndicator;
+        const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
+
+        const notifyId = adjustment.connect('notify::value', () => {
+            let translationX = (1 - adjustment.value) * 100 * page;
+            translationX = rtl ? -translationX : translationX;
+            const nextPage = this._grid.currentPage + page;
+            if (nextPage >= 0 &&
+                nextPage < this._grid.nPages - 1) {
+                const items = this._grid.getItemsAtPage(nextPage);
+                items.forEach(item => (item.translation_x = translationX));
+                indicator.set({
+                    visible: true,
+                    opacity: adjustment.value * 255,
+                    translationX,
+                });
+            }
+            this._syncClip();
+        });
+
+        this._previewedPages.set(page, {
+            adjustment,
+            notifyId,
+        });
+
+        return adjustment;
+    }
+
+    _teardownPagePreview(page) {
+        const previewedPage = this._previewedPages.get(page);
+        if (!previewedPage)
+            return;
+
+        previewedPage.adjustment.value = 1;
+        previewedPage.adjustment.disconnect(previewedPage.notifyId);
+        this._previewedPages.delete(page);
+    }
+
+    _slideSidePages(state) {
+        if (this._pagesShown === state)
+            return;
+        this._pagesShown = state;
+        const showingNextPage = state & SidePages.NEXT;
+        const showingPrevPage = state & SidePages.PREVIOUS;
+        let adjustment;
+
+        adjustment = this._getPagePreviewAdjustment(1);
+        if (showingNextPage) {
+            adjustment = this._setupPagePreview(1, state);
+
+            adjustment.ease(1, {
+                duration: PAGE_PREVIEW_ANIMATION_TIME,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+            });
+            this._updateFadeForNavigation();
+        } else if (adjustment) {
+            adjustment.ease(0, {
+                duration: PAGE_PREVIEW_ANIMATION_TIME,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+                onComplete: () => {
+                    this._teardownPagePreview(1);
+                    this._syncClip();
+                    this._nextPageIndicator.visible = false;
+                    this._updateFadeForNavigation();
+                },
+            });
+        }
+
+        adjustment = this._getPagePreviewAdjustment(-1);
+        if (showingPrevPage) {
+            adjustment = this._setupPagePreview(-1, state);
+
+            adjustment.ease(1, {
+                duration: PAGE_PREVIEW_ANIMATION_TIME,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+            });
+            this._updateFadeForNavigation();
+        } else if (adjustment) {
+            adjustment.ease(0, {
+                duration: PAGE_PREVIEW_ANIMATION_TIME,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+                onComplete: () => {
+                    this._teardownPagePreview(-1);
+                    this._syncClip();
+                    this._prevPageIndicator.visible = false;
+                    this._updateFadeForNavigation();
+                },
+            });
+        }
+    }
 });
 
 var PageManager = GObject.registerClass({


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]