diff --git a/src/gui/src/UI/Dashboard/TabApps.js b/src/gui/src/UI/Dashboard/TabApps.js index fbff12f27..7406bbb7a 100644 --- a/src/gui/src/UI/Dashboard/TabApps.js +++ b/src/gui/src/UI/Dashboard/TabApps.js @@ -2,7 +2,7 @@ import UIContextMenu from '../UIContextMenu.js'; import UIAlert from '../UIAlert.js'; import launch_app from '../../helpers/launch_app.js'; import revokeAppSessions from '../../helpers/revoke_app_sessions.js'; -import { begin_dashboard_tile_launch, settle_dashboard_tile_launch } from '../UIWindow.js'; +import { begin_dashboard_tile_launch, dashboard_tile_in_view, settle_dashboard_tile_launch } from '../UIWindow.js'; import { isTouchPrimaryDevice } from './ContextMenu/ContextMenu.js'; import { reconcileAppOrder, serializeAppOrder, mergeSavedOrder, APPS_ORDER_KV_KEY } from './appOrder.js'; import { parseRemovedApps, serializeRemovedApps, REMOVED_APPS_KV_KEY } from './removedApps.js'; @@ -654,14 +654,33 @@ function showAddAppModal ({ $el_window }) { else close(); }); - // Maximized like a tile launch, and without a morph: the add tile is not - // the app's own icon, so there is nothing for the window to grow out of. + // Opened exactly as a tile click opens it, click flourish and + // grow-out-of-the-icon morph included. The anchor is resolved HERE, once, + // so the icon half and the window half can never disagree about which + // tile this launch came from: the app's own tile when it already has one + // on screen (minimize will fly the window back to it, so that is where + // opening should come from), and otherwise the + tile the user clicked — + // which is the whole point for an app they don't have yet, since + // UIWindow's by-app-name lookup can't find a tile that doesn't exist. const openApp = appName => { if ( focusExistingAppWindow(appName) ) return; - launch_app({ name: appName, maximized: true }).catch(err => { - console.error(`Failed to launch ${appName}:`, err); - UIAlert(i18n('something_went_wrong')); - }); + const tile = dashboard_tile_in_view(appName) + || $el_window.find('.myapps-add-tile')[0] + || null; + begin_dashboard_tile_launch(tile); + launch_app({ + name: appName, + maximized: true, + window_options: { + morph_from_dashboard_tile: true, + dashboard_tile_el: tile, + }, + }) + .catch(err => { + console.error(`Failed to launch ${appName}:`, err); + UIAlert(i18n('something_went_wrong')); + }) + .finally(() => settle_dashboard_tile_launch(tile)); }; $overlay.on('click', '.myapps-add-option', function () { diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js index c0359b4ec..f4bda463e 100644 --- a/src/gui/src/UI/UIWindow.js +++ b/src/gui/src/UI/UIWindow.js @@ -716,7 +716,14 @@ async function UIWindow (options) { if ( options.morph_from_dashboard_tile && window.animate_window_opening && options.is_visible && ! options.fadeIn && ! (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) ) { - const tile = dashboard_tile_in_view(options.app); + // A named tile wins over the by-app-name lookup: the launcher knows + // which icon the user actually clicked, and it is the icon whose + // click feedback has already played. The grid's add-an-app tile uses + // this to open apps that have no tile of their own — an app you + // don't have yet can't be found by name (see showAddAppModal). + const tile = (options.dashboard_tile_el && dashboard_tile_visible(options.dashboard_tile_el)) + ? options.dashboard_tile_el + : dashboard_tile_in_view(options.app); if ( tile ) { // .window is display:none from the stylesheet until the show() // further down; the morph needs the window laid out to measure @@ -4154,23 +4161,14 @@ $.fn.focusWindow = function (event) { * the user will actually look for the app (and where opening it from will put * it back). See buildGroupTileHtml for the data-group-apps this reads. */ -function dashboard_tile_in_view (app_name) { +export function dashboard_tile_in_view (app_name) { if ( ! app_name || typeof CSS === 'undefined' || ! CSS.escape ) return null; - const in_view = tile => { - const rect = tile.getBoundingClientRect(); - if ( rect.width <= 0 || rect.height <= 0 ) return false; - const scroller = tile.closest('.myapps-pager-scroller'); - const clip = (scroller || tile.parentElement).getBoundingClientRect(); - const cx = rect.left + rect.width / 2; - const cy = rect.top + rect.height / 2; - return cx >= clip.left && cx <= clip.right && cy >= clip.top && cy <= clip.bottom; - }; const tiles = document.querySelectorAll( `.dashboard-section-apps.active .myapps-tile[data-app-name="${CSS.escape(app_name)}"]`, ); for ( const tile of tiles ) { - if ( in_view(tile) ) return tile; + if ( dashboard_tile_visible(tile) ) return tile; } const folders = document.querySelectorAll('.dashboard-section-apps.active .myapps-group-tile'); @@ -4181,11 +4179,29 @@ function dashboard_tile_in_view (app_name) { } catch ( _e ) { continue; } - if ( Array.isArray(names) && names.includes(app_name) && in_view(folder) ) return folder; + if ( Array.isArray(names) && names.includes(app_name) && dashboard_tile_visible(folder) ) return folder; } return null; } +/** + * Whether a tile is rendered AND on the pager page currently in view. The + * page test is the point: pages sit side by side in a horizontal scroller, + * so an off-page tile has a real rendered box just past the scroller's clip + * edge, and a window morphing out of one would grow from nowhere the user + * is looking. + */ +function dashboard_tile_visible (tile) { + if ( ! tile || ! tile.getBoundingClientRect ) return false; + const rect = tile.getBoundingClientRect(); + if ( rect.width <= 0 || rect.height <= 0 ) return false; + const scroller = tile.closest('.myapps-pager-scroller'); + const clip = (scroller || tile.parentElement).getBoundingClientRect(); + const cx = rect.left + rect.width / 2; + const cy = rect.top + rect.height / 2; + return cx >= clip.left && cx <= clip.right && cy >= clip.top && cy <= clip.bottom; +} + // --------------------------------------------------------------------- // Dashboard app URL ownership // ---------------------------------------------------------------------