diff --git a/data/gnome-shell-theme.gresource.xml b/data/gnome-shell-theme.gresource.xml index 038c87cf07d71fe58f0c472eda7997f7803a2d72..4eb185c5378a0d514458f85f333d4f224bb4f1dc 100644 --- a/data/gnome-shell-theme.gresource.xml +++ b/data/gnome-shell-theme.gresource.xml @@ -2,6 +2,8 @@ calendar-today.svg + carousel-arrow-next-24-symbolic.svg + carousel-arrow-back-24-symbolic.svg checkbox-focused.svg checkbox-off-focused.svg checkbox-off.svg diff --git a/data/theme/carousel-arrow-back-24-symbolic.svg b/data/theme/carousel-arrow-back-24-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..98489309223dcfa1f2a526c89ac15531d0d41063 --- /dev/null +++ b/data/theme/carousel-arrow-back-24-symbolic.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/data/theme/carousel-arrow-next-24-symbolic.svg b/data/theme/carousel-arrow-next-24-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..7d6356f239416e3a6d3b183cca8878bccdd4f39b --- /dev/null +++ b/data/theme/carousel-arrow-next-24-symbolic.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/data/theme/gnome-shell-sass/widgets/_app-grid.scss b/data/theme/gnome-shell-sass/widgets/_app-grid.scss index bbe721600def38d347b347175fbbeb6cf2168164..4b6496abc1f24f987072e9520efbc7a6b9f2b545 100644 --- a/data/theme/gnome-shell-sass/widgets/_app-grid.scss +++ b/data/theme/gnome-shell-sass/widgets/_app-grid.scss @@ -42,7 +42,7 @@ $app_grid_fg_color: #fff; .app-folder-dialog { //style like the dash border-radius: $modal_radius * 1.5; background-color: $dash_background_color; - padding: 12px; + padding: 12px 0px 12px 0px; & .folder-name-container { padding: 24px 36px 0; @@ -76,6 +76,8 @@ $app_grid_fg_color: #fff; column-spacing: $base_spacing * 5; page-padding-top: 0; page-padding-bottom: 0; + page-padding-left: 0; + page-padding-right: 0; } & .page-indicators { @@ -123,10 +125,8 @@ $app_grid_fg_color: #fff; } } -// Some hacks I don't even know -.all-apps { - // horizontal padding to make sure scrollbars or dash don't overlap content - padding: 0 88px; +.apps-scroll-view { + padding: 0; } // shutdown and other actions in the grid @@ -136,3 +136,33 @@ $app_grid_fg_color: #fff; border-radius: 99px; icon-size: $app_icon_size * 0.5; } + +.page-navigation-hint { + width: 300px; + + &.dnd { + background: rgba(255, 255, 255, 0.1); + } + + &.next:ltr, + &.previous:rtl { + background-gradient-start: rgba(255, 255, 255, 0.05); + background-gradient-end: transparent; + background-gradient-direction: horizontal; + border-radius: 15px 0px 0px 15px; + } + + &.previous:ltr, + &.next:rtl { + background-gradient-start: transparent; + background-gradient-end: rgba(255, 255, 255, 0.05); + background-gradient-direction: horizontal; + border-radius: 0px 15px 15px 0px; + } +} + +.page-navigation-arrow { + margin: 6px; + width: 24px; + height: 24px; +} diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index c3958ae9ce83120b24c2dedcaa9618c11c8ba600..0c2b9759873463bfe42173a00201cde816aa4d2c 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -38,6 +38,13 @@ var APP_ICON_TITLE_COLLAPSE_TIME = 100; const FOLDER_DIALOG_ANIMATION_TIME = 200; +const PAGE_PREVIEW_ANIMATION_TIME = 150; +const PAGE_PREVIEW_ANIMATION_START_OFFSET = 100; +const PAGE_PREVIEW_FADE_EFFECT_MAX_OFFSET = 300; +const PAGE_PREVIEW_MAX_ARROW_OFFSET = 80; +const PAGE_INDICATOR_FADE_TIME = 200; +const MAX_PAGE_PADDING = 200; + const OVERSHOOT_THRESHOLD = 20; const OVERSHOOT_TIMEOUT = 1000; @@ -48,6 +55,13 @@ const DIALOG_SHADE_HIGHLIGHT = Clutter.Color.from_pixel(0x00000055); let discreteGpuAvailable = false; +var SidePages = { + NONE: 0, + PREVIOUS: 1 << 0, + NEXT: 1 << 1, + DND: 1 << 2, +}; + function _getCategories(info) { let categoriesStr = info.get_categories(); if (!categoriesStr) @@ -129,7 +143,6 @@ var BaseAppView = GObject.registerClass({ // Standard hack for ClutterBinLayout this._grid.x_expand = true; this._grid.connect('pages-changed', () => { - this._adjustment.value = 0; this.goToPage(this._grid.currentPage); this._pageIndicators.setNPages(this._grid.nPages); this._pageIndicators.setCurrentPosition(this._grid.currentPage); @@ -137,6 +150,7 @@ var BaseAppView = GObject.registerClass({ // Scroll View this._scrollView = new St.ScrollView({ + style_class: 'apps-scroll-view', clip_to_allocation: true, x_expand: true, y_expand: true, @@ -144,10 +158,15 @@ var BaseAppView = GObject.registerClass({ enable_mouse_scrolling: false, }); this._scrollView.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.NEVER); + this._scrollView._delegate = this; 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); @@ -155,7 +174,22 @@ var BaseAppView = GObject.registerClass({ this._adjustment = scroll.adjustment; this._adjustment.connect('notify::value', adj => { this._updateFade(); - this._pageIndicators.setCurrentPosition(adj.value / adj.page_size); + const value = adj.value / adj.page_size; + this._pageIndicators.setCurrentPosition(value); + + const distanceToPage = Math.abs(Math.round(value) - value); + if (distanceToPage < 0.001) { + this._hintContainer.opacity = 255; + this._hintContainer.translationX = 0; + } else { + this._hintContainer.remove_transition('opacity'); + let opacity = Math.clamp( + 255 * (1 - (distanceToPage * 2)), + 0, 255); + + this._hintContainer.translationX = (Math.round(value) - value) * adj.page_size; + this._hintContainer.opacity = opacity; + } }); // Page Indicators @@ -171,12 +205,78 @@ 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, + }); + + // Next/prev page arrows + const rtl = this.get_text_direction() === Clutter.TextDirection.RTL; + this._nextPageArrow = new St.Icon({ + style_class: 'page-navigation-arrow', + icon_name: rtl + ? 'carousel-arrow-back-24-symbolic' + : 'carousel-arrow-next-24-symbolic', + opacity: 0, + reactive: false, + visible: false, + x_expand: true, + x_align: Clutter.ActorAlign.END, + }); + this._prevPageArrow = new St.Icon({ + style_class: 'page-navigation-arrow', + icon_name: rtl + ? 'carousel-arrow-next-24-symbolic' + : 'carousel-arrow-back-24-symbolic', + opacity: 0, + reactive: false, + visible: false, + x_expand: true, + x_align: Clutter.ActorAlign.START, + }); + + this._hintContainer = new St.Widget({ + layout_manager: new Clutter.BinLayout(), + x_expand: true, + y_expand: true, + }); + this._hintContainer.add_child(this._prevPageIndicator); + this._hintContainer.add_child(this._nextPageIndicator); + + const scrollContainer = new St.Widget({ + layout_manager: new Clutter.BinLayout(), + clip_to_allocation: true, + y_expand: true, + }); + scrollContainer.add_child(this._hintContainer); + scrollContainer.add_child(this._scrollView); + scrollContainer.add_child(this._nextPageArrow); + scrollContainer.add_child(this._prevPageArrow); + 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 +320,8 @@ var BaseAppView = GObject.registerClass({ this._dragCancelledId = 0; this.connect('destroy', this._onDestroy.bind(this)); + + this._previewedPages = new Map(); } _onDestroy() { @@ -242,9 +344,33 @@ var BaseAppView = GObject.registerClass({ this._disconnectDnD(); } + _updateFadeForNavigation() { + const fadeMargin = new Clutter.Margin(); + const rtl = this.get_text_direction() === Clutter.TextDirection.RTL; + const showingNextPage = this._pagesShown & SidePages.NEXT; + const showingPrevPage = this._pagesShown & SidePages.PREVIOUS; + + if ((showingNextPage && !rtl) || (showingPrevPage && rtl)) { + fadeMargin.right = Math.max( + -PAGE_PREVIEW_FADE_EFFECT_MAX_OFFSET, + -(this._availWidth - this._grid.layout_manager.pageWidth) / 2); + } + + if ((showingPrevPage && !rtl) || (showingNextPage && rtl)) { + fadeMargin.left = Math.max( + -PAGE_PREVIEW_FADE_EFFECT_MAX_OFFSET, + -(this._availWidth - this._grid.layout_manager.pageWidth) / 2); + } + + 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 && @@ -264,7 +390,13 @@ var BaseAppView = GObject.registerClass({ return; } - this._scrollView.update_fade_effect(vOffset, hOffset); + this._scrollView.update_fade_effect( + new Clutter.Margin({ + left: hOffset, + right: hOffset, + top: vOffset, + bottom: vOffset, + })); } _createGrid() { @@ -320,6 +452,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; @@ -344,6 +511,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, @@ -503,6 +672,7 @@ var BaseAppView = GObject.registerClass({ dragMotion: this._onDragMotion.bind(this), }; DND.addDragMonitor(this._dragMonitor); + this._slideSidePages(SidePages.PREVIOUS | SidePages.NEXT | SidePages.DND); } _onDragMotion(dragEvent) { @@ -511,6 +681,14 @@ var BaseAppView = GObject.registerClass({ const appIcon = dragEvent.source; + this._dropPage = this._pageForCoords(dragEvent.x, dragEvent.y); + if (this._dropPage && + this._dropPage === SidePages.PREVIOUS && + this._grid.currentPage === 0) { + delete this._dropPage; + return DND.DragMotionResult.NO_DROP; + } + // Handle the drag overshoot. When dragging to above the // icon grid, move to the page above; when dragging below, // move to the page below. @@ -529,12 +707,15 @@ var BaseAppView = GObject.registerClass({ } this._resetOvershoot(); + this._slideSidePages(SidePages.NONE); + delete this._dropPage; } _onDragCancelled() { // At this point, the positions aren't stored yet, thus _redisplay() // will move all items to their original positions this._redisplay(); + this._slideSidePages(SidePages.NONE); } _canAccept(source) { @@ -552,8 +733,16 @@ var BaseAppView = GObject.registerClass({ if (!this._canAccept(source)) return false; - // Dropped before the icon was moved - if (this._delayedMoveData) { + if (this._dropPage) { + const increment = this._dropPage === SidePages.NEXT ? 1 : -1; + const { currentPage, nPages } = this._grid; + const page = Math.min(currentPage + increment, nPages); + const position = page < nPages ? -1 : 0; + + this._moveItem(source, page, position); + this.goToPage(page); + } else if (this._delayedMoveData) { + // Dropped before the icon was moved const { page, position } = this._delayedMoveData; this._moveItem(source, page, position); @@ -861,12 +1050,54 @@ 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 dnd = this._pagesShown & SidePages.DND; + 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 (!dnd) { + this._prevPageArrow.visible = true; + this._prevPageArrow.ease({ + opacity, + duration, + }); + } + } + + if (showingNextPage) { + const opacity = pageNumber === this._grid.nPages - 1 ? 0 : 255; + this._nextPageIndicator.visible = true; + this._nextPageIndicator.ease({ + opacity, + duration, + }); + + if (!dnd) { + this._nextPageArrow.visible = true; + this._nextPageArrow.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); } @@ -882,10 +1113,209 @@ var BaseAppView = GObject.registerClass({ const availWidth = box.get_width(); const availHeight = box.get_height(); - this._grid.adaptToSize(availWidth, availHeight); + const gridRatio = this._grid.layout_manager.columnsPerPage / + this._grid.layout_manager.rowsPerPage; + const spaceRatio = availWidth / availHeight; + let pageWidth, pageHeight; + + if (spaceRatio > gridRatio * 1.1) { + // Enough room for some preview + pageHeight = availHeight; + pageWidth = availHeight * gridRatio; + + if (spaceRatio > gridRatio * 1.5) { + // Ultra-wide layout, give some extra space for + // the page area, but up to an extent. + const extraPageSpace = Math.min( + (availWidth - pageWidth) / 2, MAX_PAGE_PADDING); + pageWidth += extraPageSpace; + this._grid.layout_manager.pagePadding.left = extraPageSpace / 2; + this._grid.layout_manager.pagePadding.right = extraPageSpace / 2; + } + } else { + // Not enough room, needs to shrink horizontally + pageWidth = availWidth * 0.8; + pageHeight = availHeight; + this._grid.layout_manager.pagePadding.left = availWidth * 0.02; + this._grid.layout_manager.pagePadding.right = availWidth * 0.02; + } + + this._grid.adaptToSize(pageWidth, pageHeight); + + const horizontalPadding = (availWidth - this._grid.layout_manager.pageWidth) / 2; + const verticalPadding = (availHeight - this._grid.layout_manager.pageHeight) / 2; + + this._scrollView.content_padding = new Clutter.Margin({ + left: horizontalPadding, + right: horizontalPadding, + top: verticalPadding, + bottom: verticalPadding, + }); this._availWidth = availWidth; this._availHeight = availHeight; + + this._pageIndicatorOffset = horizontalPadding; + this._pageArrowOffset = Math.max( + horizontalPadding - PAGE_PREVIEW_MAX_ARROW_OFFSET, 0); + } + + _getIndicatorOffset(page, progress, baseOffset) { + const rtl = this.get_text_direction() === Clutter.TextDirection.RTL; + const translationX = + (1 - progress) * PAGE_PREVIEW_ANIMATION_START_OFFSET; + + page = rtl ? -page : page; + + return (translationX - baseOffset) * page; + } + + _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 notifyId = adjustment.connect('notify::value', () => { + const nextPage = this._grid.currentPage + page; + const hasFollowingPage = nextPage >= 0 && + nextPage < this._grid.nPages; + + if (hasFollowingPage) { + const items = this._grid.getItemsAtPage(nextPage); + items.forEach(item => { + item.translation_x = + this._getIndicatorOffset(page, adjustment.value, 0); + }); + + if (!(state & SidePages.DND)) { + const pageArrow = page > 0 + ? this._nextPageArrow + : this._prevPageArrow; + pageArrow.set({ + visible: true, + opacity: adjustment.value * 255, + translation_x: this._getIndicatorOffset( + page, adjustment.value, + this._pageArrowOffset), + }); + } + } + if (hasFollowingPage || + (page > 0 && + this._grid.layout_manager.allow_incomplete_pages && + (state & SidePages.DND) !== 0)) { + indicator.set({ + visible: true, + opacity: adjustment.value * 255, + translation_x: this._getIndicatorOffset( + page, adjustment.value, + this._pageIndicatorOffset - indicator.width), + }); + } + 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; + const dnd = state & SidePages.DND; + let adjustment; + + if (dnd) { + this._nextPageIndicator.add_style_class_name('dnd'); + this._prevPageIndicator.add_style_class_name('dnd'); + } else { + this._nextPageIndicator.remove_style_class_name('dnd'); + this._prevPageIndicator.remove_style_class_name('dnd'); + } + + 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._nextPageArrow.visible = false; + 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._prevPageArrow.visible = false; + this._prevPageIndicator.visible = false; + this._updateFadeForNavigation(); + }, + }); + } } }); @@ -962,8 +1392,6 @@ class AppDisplay extends BaseAppView { this._pageManager = new PageManager(); this._pageManager.connect('layout-changed', () => this._redisplay()); - this._scrollView.add_style_class_name('all-apps'); - this._stack = new St.Widget({ layout_manager: new Clutter.BinLayout(), x_expand: true, @@ -1045,6 +1473,7 @@ class AppDisplay extends BaseAppView { const [, indicatorHeight] = this._pageIndicators.get_preferred_height(-1); height -= indicatorHeight; + this._grid.findBestModeForSize(width, height); super.adaptToSize(width, height); } diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js index e8ac2b07edbdb148119b0c7a8f2d8db9b13d9dc1..2c71c29c107613ee695b1b729772c743a506d728 100644 --- a/js/ui/iconGrid.js +++ b/js/ui/iconGrid.js @@ -1223,7 +1223,7 @@ var IconGrid = GObject.registerClass({ } } - _findBestModeForSize(width, height) { + findBestModeForSize(width, height) { const { pagePadding } = this.layout_manager; width -= pagePadding.left + pagePadding.right; height -= pagePadding.top + pagePadding.bottom; @@ -1446,7 +1446,6 @@ var IconGrid = GObject.registerClass({ } adaptToSize(width, height) { - this._findBestModeForSize(width, height); this.layout_manager.adaptToSize(width, height); } diff --git a/src/st/st-scroll-view-fade.c b/src/st/st-scroll-view-fade.c index a0bb85bda82fab513fa4b4584b4d2cced13c06f3..b387e384a56883ed467dbe93023c21bb504b947e 100644 --- a/src/st/st-scroll-view-fade.c +++ b/src/st/st-scroll-view-fade.c @@ -46,8 +46,7 @@ struct _StScrollViewFade guint fade_edges : 1; - float vfade_offset; - float hfade_offset; + ClutterMargin fade_margins; }; G_DEFINE_TYPE (StScrollViewFade, @@ -57,8 +56,7 @@ G_DEFINE_TYPE (StScrollViewFade, enum { PROP_0, - PROP_VFADE_OFFSET, - PROP_HFADE_OFFSET, + PROP_FADE_MARGINS, PROP_FADE_EDGES, N_PROPS @@ -96,9 +94,10 @@ st_scroll_view_fade_paint_target (ClutterOffscreenEffect *effect, gdouble value, lower, upper, page_size; ClutterActor *vscroll = st_scroll_view_get_vscroll_bar (ST_SCROLL_VIEW (self->actor)); ClutterActor *hscroll = st_scroll_view_get_hscroll_bar (ST_SCROLL_VIEW (self->actor)); - gboolean h_scroll_visible, v_scroll_visible; + gboolean h_scroll_visible, v_scroll_visible, rtl; ClutterActorBox allocation, content_box, paint_box; + ClutterMargin *content_padding; float fade_area_topleft[2]; float fade_area_bottomright[2]; @@ -110,6 +109,13 @@ st_scroll_view_fade_paint_target (ClutterOffscreenEffect *effect, clutter_actor_get_allocation_box (self->actor, &allocation); st_theme_node_get_content_box (st_widget_get_theme_node (ST_WIDGET (self->actor)), (const ClutterActorBox *)&allocation, &content_box); + g_object_get (self->actor, "content-padding", &content_padding, NULL); + + content_box.x1 += content_padding->left; + content_box.x2 -= content_padding->right; + content_box.y1 += content_padding->top; + content_box.y2 -= content_padding->bottom; + clutter_margin_free (content_padding); /* * The FBO is based on the paint_volume's size which can be larger then the actual @@ -136,6 +142,15 @@ st_scroll_view_fade_paint_target (ClutterOffscreenEffect *effect, if (h_scroll_visible) fade_area_bottomright[1] -= clutter_actor_get_height (hscroll); + if (self->fade_margins.left < 0) + fade_area_topleft[0] -= ABS (self->fade_margins.left); + if (self->fade_margins.right < 0) + fade_area_bottomright[0] += ABS (self->fade_margins.right); + if (self->fade_margins.top < 0) + fade_area_topleft[1] -= ABS (self->fade_margins.top); + if (self->fade_margins.bottom < 0) + fade_area_bottomright[1] += ABS (self->fade_margins.bottom); + st_adjustment_get_values (self->vadjustment, &value, &lower, &upper, NULL, NULL, &page_size); value = (value - lower) / (upper - page_size - lower); clutter_shader_effect_set_uniform (shader, "fade_edges_top", G_TYPE_INT, 1, self->fade_edges ? value >= 0.0 : value > 0.0); @@ -143,11 +158,20 @@ st_scroll_view_fade_paint_target (ClutterOffscreenEffect *effect, st_adjustment_get_values (self->hadjustment, &value, &lower, &upper, NULL, NULL, &page_size); value = (value - lower) / (upper - page_size - lower); - clutter_shader_effect_set_uniform (shader, "fade_edges_left", G_TYPE_INT, 1, self->fade_edges ? value >= 0.0 : value > 0.0); - clutter_shader_effect_set_uniform (shader, "fade_edges_right", G_TYPE_INT, 1, self->fade_edges ? value <= 1.0 : value < 1.0); - - clutter_shader_effect_set_uniform (shader, "vfade_offset", G_TYPE_FLOAT, 1, self->vfade_offset); - clutter_shader_effect_set_uniform (shader, "hfade_offset", G_TYPE_FLOAT, 1, self->hfade_offset); + rtl = clutter_actor_get_text_direction (self->actor) == CLUTTER_TEXT_DIRECTION_RTL; + clutter_shader_effect_set_uniform (shader, "fade_edges_left", G_TYPE_INT, 1, + self->fade_edges ? + value >= 0.0 : + (rtl ? value < 1.0 : value > 0.0)); + clutter_shader_effect_set_uniform (shader, "fade_edges_right", G_TYPE_INT, 1, + self->fade_edges ? + value <= 1.0 : + (rtl ? value > 0.0 : value < 1.0)); + + clutter_shader_effect_set_uniform (shader, "fade_offset_top", G_TYPE_FLOAT, 1, ABS (self->fade_margins.top)); + clutter_shader_effect_set_uniform (shader, "fade_offset_bottom", G_TYPE_FLOAT, 1, ABS (self->fade_margins.bottom)); + clutter_shader_effect_set_uniform (shader, "fade_offset_left", G_TYPE_FLOAT, 1, ABS (self->fade_margins.left)); + clutter_shader_effect_set_uniform (shader, "fade_offset_right", G_TYPE_FLOAT, 1, ABS (self->fade_margins.right)); clutter_shader_effect_set_uniform (shader, "tex", G_TYPE_INT, 1, 0); clutter_shader_effect_set_uniform (shader, "height", G_TYPE_FLOAT, 1, clutter_actor_get_height (self->actor)); clutter_shader_effect_set_uniform (shader, "width", G_TYPE_FLOAT, 1, clutter_actor_get_width (self->actor)); @@ -266,39 +290,21 @@ st_scroll_view_fade_dispose (GObject *gobject) } static void -st_scroll_view_vfade_set_offset (StScrollViewFade *self, - float fade_offset) -{ - if (self->vfade_offset == fade_offset) - return; - - g_object_freeze_notify (G_OBJECT (self)); - - self->vfade_offset = fade_offset; - - if (self->actor != NULL) - clutter_actor_queue_redraw (self->actor); - - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VFADE_OFFSET]); - g_object_thaw_notify (G_OBJECT (self)); -} - -static void -st_scroll_view_hfade_set_offset (StScrollViewFade *self, - float fade_offset) +st_scroll_view_set_fade_margins (StScrollViewFade *self, + ClutterMargin *fade_margins) { - if (self->hfade_offset == fade_offset) + if (self->fade_margins.left == fade_margins->left && + self->fade_margins.right == fade_margins->right && + self->fade_margins.top == fade_margins->top && + self->fade_margins.bottom == fade_margins->bottom) return; - g_object_freeze_notify (G_OBJECT (self)); - - self->hfade_offset = fade_offset; + self->fade_margins = *fade_margins; if (self->actor != NULL) clutter_actor_queue_redraw (self->actor); - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HFADE_OFFSET]); - g_object_thaw_notify (G_OBJECT (self)); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FADE_MARGINS]); } static void @@ -329,11 +335,8 @@ st_scroll_view_fade_set_property (GObject *object, switch (prop_id) { - case PROP_VFADE_OFFSET: - st_scroll_view_vfade_set_offset (self, g_value_get_float (value)); - break; - case PROP_HFADE_OFFSET: - st_scroll_view_hfade_set_offset (self, g_value_get_float (value)); + case PROP_FADE_MARGINS: + st_scroll_view_set_fade_margins (self, g_value_get_boxed (value)); break; case PROP_FADE_EDGES: st_scroll_view_fade_set_fade_edges (self, g_value_get_boolean (value)); @@ -354,11 +357,8 @@ st_scroll_view_fade_get_property (GObject *object, switch (prop_id) { - case PROP_HFADE_OFFSET: - g_value_set_float (value, self->hfade_offset); - break; - case PROP_VFADE_OFFSET: - g_value_set_float (value, self->vfade_offset); + case PROP_FADE_MARGINS: + g_value_set_boxed (value, &self->fade_margins); break; case PROP_FADE_EDGES: g_value_set_boolean (value, self->fade_edges); @@ -391,29 +391,15 @@ st_scroll_view_fade_class_init (StScrollViewFadeClass *klass) offscreen_class->paint_target = st_scroll_view_fade_paint_target; /** - * StScrollViewFade:vfade-offset: - * - * The height of area which is faded at the top and bottom edges of the - * #StScrollViewFade. - */ - props[PROP_VFADE_OFFSET] = - g_param_spec_float ("vfade-offset", - "Vertical Fade Offset", - "The height of the area which is faded at the edge", - 0.f, G_MAXFLOAT, DEFAULT_FADE_OFFSET, - ST_PARAM_READWRITE); - - /** - * StScrollViewFade:hfade-offset: + * StScrollViewFade:fade-margins: * - * The height of area which is faded at the left and right edges of the - * #StScrollViewFade. + * The margins widths that are faded. */ - props[PROP_HFADE_OFFSET] = - g_param_spec_float ("hfade-offset", - "Horizontal Fade Offset", - "The width of the area which is faded at the edge", - 0.f, G_MAXFLOAT, DEFAULT_FADE_OFFSET, + props[PROP_FADE_MARGINS] = + g_param_spec_boxed ("fade-margins", + "Fade margins", + "The margin widths that are faded", + CLUTTER_TYPE_MARGIN, ST_PARAM_READWRITE); /** @@ -434,8 +420,12 @@ st_scroll_view_fade_class_init (StScrollViewFadeClass *klass) static void st_scroll_view_fade_init (StScrollViewFade *self) { - self->vfade_offset = DEFAULT_FADE_OFFSET; - self->hfade_offset = DEFAULT_FADE_OFFSET; + self->fade_margins = (ClutterMargin) { + DEFAULT_FADE_OFFSET, + DEFAULT_FADE_OFFSET, + DEFAULT_FADE_OFFSET, + DEFAULT_FADE_OFFSET, + }; } /** diff --git a/src/st/st-scroll-view-fade.glsl b/src/st/st-scroll-view-fade.glsl index e3f1a259f36c020a8df15e252943a9232a24ac26..264f3bcd7228e757fd92aa206d93b54bd7592fb6 100644 --- a/src/st/st-scroll-view-fade.glsl +++ b/src/st/st-scroll-view-fade.glsl @@ -20,8 +20,10 @@ uniform sampler2D tex; uniform float height; uniform float width; -uniform float vfade_offset; -uniform float hfade_offset; +uniform float fade_offset_top; +uniform float fade_offset_bottom; +uniform float fade_offset_left; +uniform float fade_offset_right; uniform bool fade_edges_top; uniform bool fade_edges_right; uniform bool fade_edges_bottom; @@ -37,23 +39,19 @@ void main () float y = height * cogl_tex_coord_in[0].y; float x = width * cogl_tex_coord_in[0].x; - /* - * We cannot just return here due to a bug in llvmpipe see: - * https://bugzilla.freedesktop.org/show_bug.cgi?id=62357 - */ if (x > fade_area_topleft[0] && x < fade_area_bottomright[0] && - y > fade_area_topleft[1] && y < fade_area_bottomright[1]) { + y > fade_area_topleft[1] && y < fade_area_bottomright[1]) + { float ratio = 1.0; - float fade_top_start = fade_area_topleft[1] + vfade_offset; - float fade_left_start = fade_area_topleft[0] + hfade_offset; - float fade_bottom_start = fade_area_bottomright[1] - vfade_offset; - float fade_right_start = fade_area_bottomright[0] - hfade_offset; - bool fade_top = y < vfade_offset && fade_edges_top; + float fade_top_start = fade_area_topleft[1] + fade_offset_top; + float fade_left_start = fade_area_topleft[0] + fade_offset_left; + float fade_bottom_start = fade_area_bottomright[1] - fade_offset_bottom; + float fade_right_start = fade_area_bottomright[0] - fade_offset_right; + bool fade_top = y < fade_top_start && fade_edges_top; bool fade_bottom = y > fade_bottom_start && fade_edges_bottom; bool fade_left = x < fade_left_start && fade_edges_left; bool fade_right = x > fade_right_start && fade_edges_right; - float vfade_scale = height / vfade_offset; if (fade_top) { ratio *= (fade_area_topleft[1] - y) / (fade_area_topleft[1] - fade_top_start); } @@ -62,7 +60,6 @@ void main () ratio *= (fade_area_bottomright[1] - y) / (fade_area_bottomright[1] - fade_bottom_start); } - float hfade_scale = width / hfade_offset; if (fade_left) { ratio *= (fade_area_topleft[0] - x) / (fade_area_topleft[0] - fade_left_start); } @@ -72,5 +69,7 @@ void main () } cogl_color_out *= ratio; + } else { + cogl_color_out *= 0; } } diff --git a/src/st/st-scroll-view.c b/src/st/st-scroll-view.c index 0cf48adafc4fa768dcec00d6cd1196c6ea0fd462..606701565d12b327cb099bb9a55b2f1e5032060b 100644 --- a/src/st/st-scroll-view.c +++ b/src/st/st-scroll-view.c @@ -84,6 +84,8 @@ struct _StScrollViewPrivate StAdjustment *vadjustment; ClutterActor *vscroll; + ClutterMargin content_padding; + StPolicyType hscrollbar_policy; StPolicyType vscrollbar_policy; @@ -116,6 +118,7 @@ enum { PROP_VSCROLLBAR_VISIBLE, PROP_MOUSE_SCROLL, PROP_OVERLAY_SCROLLBARS, + PROP_CONTENT_PADDING, N_PROPS }; @@ -156,6 +159,9 @@ st_scroll_view_get_property (GObject *object, case PROP_OVERLAY_SCROLLBARS: g_value_set_boolean (value, priv->overlay_scrollbars); break; + case PROP_CONTENT_PADDING: + g_value_set_boxed (value, &priv->content_padding); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } @@ -164,42 +170,41 @@ st_scroll_view_get_property (GObject *object, /** * st_scroll_view_update_fade_effect: * @scroll: a #StScrollView - * @vfade_offset: The length of the vertical fade effect, in pixels. - * @hfade_offset: The length of the horizontal fade effect, in pixels. + * @fade_margins: a #ClutterMargin defining the vertical fade effects, in pixels. * - * Sets the height of the fade area area in pixels. A value of 0 + * Sets the fade effects in all four edges of the view. A value of 0 * disables the effect. */ void -st_scroll_view_update_fade_effect (StScrollView *scroll, - float vfade_offset, - float hfade_offset) +st_scroll_view_update_fade_effect (StScrollView *scroll, + ClutterMargin *fade_margins) { StScrollViewPrivate *priv = ST_SCROLL_VIEW (scroll)->priv; - /* A fade amount of more than 0 enables the effect. */ - if (vfade_offset > 0. || hfade_offset > 0.) + /* A fade amount of other than 0 enables the effect. */ + if (fade_margins->left != 0. || fade_margins->right != 0. || + fade_margins->top != 0. || fade_margins->bottom != 0.) { - if (priv->fade_effect == NULL) { - priv->fade_effect = g_object_new (ST_TYPE_SCROLL_VIEW_FADE, NULL); + if (priv->fade_effect == NULL) + { + priv->fade_effect = g_object_new (ST_TYPE_SCROLL_VIEW_FADE, NULL); - clutter_actor_add_effect_with_name (CLUTTER_ACTOR (scroll), "fade", - CLUTTER_EFFECT (priv->fade_effect)); - } + clutter_actor_add_effect_with_name (CLUTTER_ACTOR (scroll), "fade", + CLUTTER_EFFECT (priv->fade_effect)); + } g_object_set (priv->fade_effect, - "vfade-offset", vfade_offset, - NULL); - g_object_set (priv->fade_effect, - "hfade-offset", hfade_offset, + "fade-margins", fade_margins, NULL); } else { - if (priv->fade_effect != NULL) { - clutter_actor_remove_effect (CLUTTER_ACTOR (scroll), CLUTTER_EFFECT (priv->fade_effect)); - priv->fade_effect = NULL; - } + if (priv->fade_effect != NULL) + { + clutter_actor_remove_effect (CLUTTER_ACTOR (scroll), + CLUTTER_EFFECT (priv->fade_effect)); + priv->fade_effect = NULL; + } } clutter_actor_queue_redraw (CLUTTER_ACTOR (scroll)); @@ -234,6 +239,9 @@ st_scroll_view_set_property (GObject *object, priv->hscrollbar_policy, g_value_get_enum (value)); break; + case PROP_CONTENT_PADDING: + priv->content_padding = * (ClutterMargin *) g_value_get_boxed (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } @@ -546,6 +554,11 @@ st_scroll_view_allocate (ClutterActor *actor, st_theme_node_get_content_box (theme_node, box, &content_box); + content_box.x1 += priv->content_padding.left; + content_box.x2 -= priv->content_padding.right; + content_box.y1 += priv->content_padding.top; + content_box.y2 += priv->content_padding.bottom; + avail_width = content_box.x2 - content_box.x1; avail_height = content_box.y2 - content_box.y1; @@ -744,7 +757,13 @@ st_scroll_view_style_changed (StWidget *widget) StThemeNode *theme_node = st_widget_get_theme_node (widget); gdouble vfade_offset = st_theme_node_get_length (theme_node, "-st-vfade-offset"); gdouble hfade_offset = st_theme_node_get_length (theme_node, "-st-hfade-offset"); - st_scroll_view_update_fade_effect (self, vfade_offset, hfade_offset); + st_scroll_view_update_fade_effect (self, + &(ClutterMargin) { + .top = vfade_offset, + .bottom = vfade_offset, + .left = hfade_offset, + .right = hfade_offset, + }); st_widget_style_changed (ST_WIDGET (priv->hscroll)); st_widget_style_changed (ST_WIDGET (priv->vscroll)); @@ -928,6 +947,13 @@ st_scroll_view_class_init (StScrollViewClass *klass) FALSE, ST_PARAM_READWRITE); + props[PROP_CONTENT_PADDING] = + g_param_spec_boxed ("content-padding", + "Content padding", + "Content padding", + CLUTTER_TYPE_MARGIN, + ST_PARAM_READWRITE); + g_object_class_install_properties (object_class, N_PROPS, props); } diff --git a/src/st/st-scroll-view.h b/src/st/st-scroll-view.h index 2f2c31d6c925cd54d3176cde4075b2ca66712482..e2acaca2f599c9b3a9eafe067c8e2897e9e7d193 100644 --- a/src/st/st-scroll-view.h +++ b/src/st/st-scroll-view.h @@ -82,9 +82,8 @@ gboolean st_scroll_view_get_overlay_scrollbars (StScrollView *scroll); void st_scroll_view_set_policy (StScrollView *scroll, StPolicyType hscroll, StPolicyType vscroll); -void st_scroll_view_update_fade_effect (StScrollView *scroll, - float vfade_offset, - float hfade_offset); +void st_scroll_view_update_fade_effect (StScrollView *scroll, + ClutterMargin *fade_margins); G_END_DECLS diff --git a/src/st/st-viewport.c b/src/st/st-viewport.c index c663ae4cd311db9143a84c43ae6b52e51c958ca8..e2593196e8ddd6e992c6acbb5341bf64bc4dac17 100644 --- a/src/st/st-viewport.c +++ b/src/st/st-viewport.c @@ -55,6 +55,7 @@ static void st_viewport_scrollable_interface_init (StScrollableInterface *iface) enum { PROP_0, + PROP_CLIP_TO_VIEW, PROP_HADJUST, PROP_VADJUST }; @@ -63,6 +64,7 @@ typedef struct { StAdjustment *hadjustment; StAdjustment *vadjustment; + gboolean clip_to_view; } StViewportPrivate; G_DEFINE_TYPE_WITH_CODE (StViewport, st_viewport, ST_TYPE_WIDGET, @@ -163,12 +165,28 @@ st_viewport_scrollable_interface_init (StScrollableInterface *iface) iface->get_adjustments = scrollable_get_adjustments; } +static void +st_viewport_set_clip_to_view (StViewport *viewport, + gboolean clip_to_view) +{ + StViewportPrivate *priv = + st_viewport_get_instance_private (viewport); + + if (!!priv->clip_to_view != !!clip_to_view) + { + priv->clip_to_view = clip_to_view; + clutter_actor_queue_redraw (CLUTTER_ACTOR (viewport)); + } +} + static void st_viewport_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { + StViewportPrivate *priv = + st_viewport_get_instance_private (ST_VIEWPORT (object)); StAdjustment *adjustment; switch (property_id) @@ -183,6 +201,10 @@ st_viewport_get_property (GObject *object, g_value_set_object (value, adjustment); break; + case PROP_CLIP_TO_VIEW: + g_value_set_boolean (value, priv->clip_to_view); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } @@ -212,6 +234,10 @@ st_viewport_set_property (GObject *object, g_value_get_object (value)); break; + case PROP_CLIP_TO_VIEW: + st_viewport_set_clip_to_view (viewport, g_value_get_boolean (value)); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } @@ -405,7 +431,7 @@ st_viewport_paint (ClutterActor *actor, /* The content area forms the viewport into the scrolled contents, while * the borders and background stay in place; after drawing the borders and * background, we clip to the content area */ - if (priv->hadjustment || priv->vadjustment) + if (priv->clip_to_view && (priv->hadjustment || priv->vadjustment)) { cogl_framebuffer_push_rectangle_clip (fb, (int)content_box.x1, @@ -419,7 +445,7 @@ st_viewport_paint (ClutterActor *actor, child = clutter_actor_get_next_sibling (child)) clutter_actor_paint (child, paint_context); - if (priv->hadjustment || priv->vadjustment) + if (priv->clip_to_view && (priv->hadjustment || priv->vadjustment)) cogl_framebuffer_pop_clip (fb); } @@ -478,6 +504,9 @@ st_viewport_get_paint_volume (ClutterActor *actor, if (!clutter_actor_has_allocation (actor)) return FALSE; + if (!priv->clip_to_view) + return CLUTTER_ACTOR_CLASS (st_viewport_parent_class)->get_paint_volume (actor, volume); + /* When have an adjustment we are clipped to the content box, so base * our paint volume on that. */ if (priv->hadjustment || priv->vadjustment) @@ -558,6 +587,14 @@ st_viewport_class_init (StViewportClass *klass) actor_class->get_paint_volume = st_viewport_get_paint_volume; actor_class->pick = st_viewport_pick; + g_object_class_install_property (object_class, + PROP_CLIP_TO_VIEW, + g_param_spec_boolean ("clip-to-view", + "Clip to view", + "Clip to view", + TRUE, + ST_PARAM_READWRITE)); + /* StScrollable properties */ g_object_class_override_property (object_class, PROP_HADJUST, @@ -566,10 +603,13 @@ st_viewport_class_init (StViewportClass *klass) g_object_class_override_property (object_class, PROP_VADJUST, "vadjustment"); - } static void st_viewport_init (StViewport *self) { + StViewportPrivate *priv = + st_viewport_get_instance_private (self); + + priv->clip_to_view = TRUE; }