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




commit 7adfce53948379df38c4492fe806a8f2e3fe2b10
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.

 data/theme/gnome-shell-sass/widgets/_app-grid.scss |  14 ++
 js/ui/appDisplay.js                                | 226 ++++++++++++++++++++-
 2 files changed, 239 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 051dbc239e..eb9a3f4b91 100644
--- a/data/theme/gnome-shell-sass/widgets/_app-grid.scss
+++ b/data/theme/gnome-shell-sass/widgets/_app-grid.scss
@@ -137,3 +137,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 628f235ccb..6679bfd6b7 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)
@@ -148,6 +158,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);
 
@@ -171,12 +185,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
@@ -220,6 +266,8 @@ var BaseAppView = GObject.registerClass({
         this._dragCancelledId = 0;
 
         this.connect('destroy', this._onDestroy.bind(this));
+
+        this._previewedPages = new Map();
     }
 
     _onDestroy() {
@@ -242,9 +290,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 &&
@@ -326,6 +386,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;
@@ -875,6 +970,20 @@ var BaseAppView = GObject.registerClass({
         if (this._grid.currentPage === pageNumber)
             return;
 
+        if (animate && (this._pagesShown & SidePages.PREVIOUS) !== 0) {
+            this._prevPageIndicator.ease({
+                duration: PAGE_INDICATOR_FADE_TIME,
+                opacity: pageNumber === 0 ? 0 : 255,
+            });
+        }
+
+        if (animate && (this._pagesShown & SidePages.NEXT) !== 0) {
+            this._nextPageIndicator.ease({
+                duration: PAGE_INDICATOR_FADE_TIME,
+                opacity: pageNumber === this._grid.nPages - 1 ? 0 : 255,
+            });
+        }
+
         this._grid.goToPage(pageNumber, animate);
     }
 
@@ -895,6 +1004,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]