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




commit 2234456ced060e3d759e3898160250aeccb90777
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                                | 231 ++++++++++++++++++++-
 2 files changed, 244 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..31de33d086 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,46 @@ var BaseAppView = GObject.registerClass({
             this._scrollView.event(event, false);
         });
 
+        // Navigation indicators
+        this._nextPageIndicator = new St.Widget({
+            opacity: 0,
+            visible: false,
+            reactive: false,
+            x_expand: true,
+            y_expand: true,
+            x_align: Clutter.ActorAlign.END,
+            y_align: Clutter.ActorAlign.FILL,
+        });
+        this._nextPageIndicator.add_style_class_name('page-navigation-hint');
+        this._nextPageIndicator.add_style_class_name('next');
+
+        this._prevPageIndicator = new St.Widget({
+            opacity: 0,
+            visible: false,
+            reactive: false,
+            x_expand: true,
+            y_expand: true,
+            x_align: Clutter.ActorAlign.START,
+            y_align: Clutter.ActorAlign.FILL,
+        });
+        this._prevPageIndicator.add_style_class_name('page-navigation-hint');
+        this._prevPageIndicator.add_style_class_name('previous');
+
+        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 +268,8 @@ var BaseAppView = GObject.registerClass({
         this._dragCancelledId = 0;
 
         this.connect('destroy', this._onDestroy.bind(this));
+
+        this._previewedPages = new Map();
     }
 
     _onDestroy() {
@@ -242,9 +292,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 +388,54 @@ var BaseAppView = GObject.registerClass({
         return Clutter.EVENT_STOP;
     }
 
+    _pageForCoords(x, y) {
+        const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
+        const alloc = this._grid.get_allocation_box();
+
+        const [success, pointerX] = this._scrollView.transform_stage_point(x, y);
+        if (!success)
+            return SidePages.NONE;
+
+        if (pointerX < alloc.x1)
+            return rtl ? SidePages.NEXT : SidePages.PREVIOUS;
+        else if (pointerX > alloc.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._grid.currentPage < this._grid.nPages - 1)
+            this._grid.currentPage++;
+        else if (page === SidePages.PREVIOUS && this._grid.currentPage > 0)
+            this._grid.currentPage--;
+
+        if (this._grid.currentPage === 0) {
+            this._prevPageIndicator.ease({
+                duration: PAGE_INDICATOR_FADE_TIME,
+                opacity: 0,
+            });
+        } else if (this._grid.currentPage === this._grid.nPages - 1) {
+            this._nextPageIndicator.ease({
+                duration: PAGE_INDICATOR_FADE_TIME,
+                opacity: 0,
+            });
+        }
+    }
+
+    _onLeave() {
+        this._slideSidePages(SidePages.NONE);
+    }
+
     _swipeBegin(tracker, monitor) {
         if (monitor !== Main.layoutManager.primaryIndex)
             return;
@@ -895,6 +1005,125 @@ var BaseAppView = GObject.registerClass({
         this._availWidth = availWidth;
         this._availHeight = availHeight;
     }
+
+    _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) {
+        const multiplier = page;
+        if (this._previewedPages.has(page))
+            return;
+
+        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 * multiplier;
+            translationX = rtl ? -translationX : translationX;
+            const nextPage = this._grid.currentPage + page;
+            if (nextPage >= 0 &&
+                nextPage < this._grid.nPages - 1) {
+                const items = this._grid.layout_manager.getItemsAtPage(nextPage);
+                items.forEach(item => (item.translation_x = translationX));
+                indicator.visible = true;
+                indicator.opacity = adjustment.value * 255;
+                indicator.translation_x = translationX;
+            }
+            this._syncClip();
+        });
+
+        this._previewedPages.set(page, {
+            adjustment,
+            notifyId,
+        });
+    }
+
+    _teardownPagePreview(page) {
+        const previewedPage = this._previewedPages.get(page);
+        if (!previewedPage)
+            return;
+
+        previewedPage.adjustment.value = 1;
+        previewedPage.adjustment.disconnect(previewedPage.notifyId);
+        this._previewedPages.delete(page);
+    }
+
+    _getPagePreviewAdjustment(page) {
+        const previewedPage = this._previewedPages.get(page);
+        return previewedPage?.adjustment;
+    }
+
+    _slideSidePages(state) {
+        if (this._pagesShown === state)
+            return;
+        this._pagesShown = state;
+        const showingNextPage = state & SidePages.NEXT;
+        const showingPrevPage = state & SidePages.PREVIOUS;
+
+        if (showingNextPage) {
+            this._setupPagePreview(1, state);
+            const adjustment = this._getPagePreviewAdjustment(1);
+
+            adjustment.ease(1, {
+                duration: PAGE_PREVIEW_ANIMATION_TIME,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+            });
+            this._updateFadeForNavigation();
+        } else {
+            const adjustment = this._getPagePreviewAdjustment(1);
+
+            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();
+                    },
+                });
+            }
+        }
+
+        if (showingPrevPage) {
+            this._setupPagePreview(-1, state);
+            const adjustment = this._getPagePreviewAdjustment(-1);
+
+            adjustment.ease(1, {
+                duration: PAGE_PREVIEW_ANIMATION_TIME,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+            });
+            this._updateFadeForNavigation();
+        } else {
+            const adjustment = this._getPagePreviewAdjustment(-1);
+
+            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]