/**
 * Vertical Workspaces
 * overviewControls.js
 * 
 * @author     GdH <G-dH@github.com>
 * @copyright  2022 - 2023
 * @license    GPL-3.0
 *
 */

'use strict';

const { Clutter, GLib, GObject, Graphene, Meta, Shell, St } = imports.gi;
const Main = imports.ui.main;
const Util = imports.misc.util;
const OverviewControls = imports.ui.overviewControls;
const WorkspaceThumbnail = imports.ui.workspaceThumbnail;

const ControlsState = imports.ui.overviewControls.ControlsState;
const FitMode = imports.ui.workspacesView.FitMode;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();

const _Util = Me.imports.util;
let _overrides;

const ANIMATION_TIME = imports.ui.overview.ANIMATION_TIME;
const DASH_MAX_SIZE_RATIO = 0.15;

let _originalSearchControllerSigId;
let _searchControllerSigId;
let _startupAnimTimeoutId1;
let _startupAnimTimeoutId2;
let _updateAppGridTimeoutId;
let _startupInitComplete = false;

let opt;


function update(reset = false) {
    
    if (_overrides) {
        _overrides.removeAll();
    }
    
    _replaceOnSearchChanged(reset);
    
    if (reset) {
        _overrides = null;
        opt = null;
        return;
    }
    
    opt = Me.imports.settings.opt;

    _overrides = new _Util.Overrides();

    _overrides.addOverride('ControlsManager', OverviewControls.ControlsManager.prototype, ControlsManager);
 
    if (opt.ORIENTATION === Clutter.Orientation.VERTICAL) {
        _overrides.addOverride('ControlsManagerLayout', OverviewControls.ControlsManagerLayout.prototype, ControlsManagerLayoutVertical);
    } else {
        _overrides.addOverride('ControlsManagerLayout', OverviewControls.ControlsManagerLayout.prototype, ControlsManagerLayoutHorizontal);
    }
}

function _dashNotDefault() {
    return Main.overview.dash !== Main.overview._overview._controls.layoutManager._dash;
}

function _dashIsDashToDock() {
    return Main.overview.dash._isHorizontal !== undefined;
}

function _replaceOnSearchChanged(reset = false) {
    const searchController = Main.overview._overview.controls._searchController;
    if (reset) {
        if (_searchControllerSigId) {
            searchController.disconnect(_searchControllerSigId);
            _searchControllerSigId = 0;
        }
        if (_originalSearchControllerSigId) {
            searchController.unblock_signal_handler(_originalSearchControllerSigId);
            _originalSearchControllerSigId = 0;
        }
    } else {
        // reconnect signal to use custom function (callbacks cannot be overridden in class prototype, they are already in memory as a copy for the given callback)
        _originalSearchControllerSigId = GObject.signal_handler_find(searchController, { signalId: 'notify', detail: 'search-active' });
        if (_originalSearchControllerSigId) {
            searchController.block_signal_handler(_originalSearchControllerSigId);
        }
        _searchControllerSigId = searchController.connect('notify::search-active', ControlsManager._onSearchChanged.bind(Main.overview._overview.controls));
    }

}

var ControlsManager = {
    // this function is used as a callback by a signal handler, needs to be reconnected after modification as the original callback uses a copy of the original function
    /*_update: function() {
        ...
    }*/

    // this function has duplicate in WorkspaceView so we use one function for both to avoid issues with syncing them
    _getFitModeForState: function(state) {
        return _getFitModeForState(state);
    },

    _updateThumbnailsBox: function() {
        const { shouldShow } = this._thumbnailsBox;
        const thumbnailsBoxVisible = shouldShow;
        this._thumbnailsBox.visible = thumbnailsBoxVisible;

        // this call should be directly in _update(), but it's used as a callback function and it would require to reconnect the signal
        this._updateWorkspacesDisplay();
    },

    // this function is pure addition to the original code and handles wsDisp transition to APP_GRID view
    _updateWorkspacesDisplay: function() {
        this._workspacesDisplay.translation_x = 0;
        this._workspacesDisplay.translation_y = 0;
        this._workspacesDisplay.scale_x = 1;
        this._workspacesDisplay.scale_y = 1;
        const { initialState, finalState, progress, currentState } = this._stateAdjustment.getStateTransitionParams();
        const { searchActive } = this._searchController;

        const paramsForState = s => {
            let opacity;
            switch (s) {
            case ControlsState.HIDDEN:
            case ControlsState.WINDOW_PICKER:
                opacity = 255;
                break;
            case ControlsState.APP_GRID:
                opacity = 0;
                break;
            default:
                opacity = 255;
                break;
            }
            return { opacity };
        };

        let initialParams = paramsForState(initialState);
        let finalParams = paramsForState(finalState);

        let opacity = Math.round(Util.lerp(initialParams.opacity, finalParams.opacity, progress));

        let workspacesDisplayVisible = (opacity != 0)/* && !(searchActive)*/;

        // improve transition from search results to desktop
        if (finalState === 0 && this._searchController._searchResults.visible) {
            this._searchController.hide();
        }

        // reset Static Workspace window picker mode
        if (currentState === 0/*finalState === 0 && progress === 1*/ && opt.OVERVIEW_MODE && opt.WORKSPACE_MODE) {
            opt.WORKSPACE_MODE = 0;
        }

        if (!opt.WS_ANIMATION || !opt.SHOW_WS_TMB) {
            this._workspacesDisplay.opacity = opacity;
        } else if (!opt.SHOW_WS_TMB_BG) {
            // fade out ws wallpaper during transition to ws switcher if ws switcher background disabled
            const ws = this._workspacesDisplay._workspacesViews[global.display.get_primary_monitor()]._workspaces[this._workspaceAdjustment.value];
            if (ws)
                ws._background.opacity = opacity;
        }

        // if ws preview background is disabled, animate tmb box and dash
        const tmbBox = this._thumbnailsBox;
        const dash = this.dash;
        const searchEntryBin = this._searchEntryBin;
        // this dash transition collides with startup animation and freezes GS for good, needs to be delayed (first Main.overview 'hiding' event enables it)
        const skipDash = _dashNotDefault();

        // OVERVIEW_MODE 2 should animate dash and wsTmbBox only if WORKSPACE_MODE === 0 (windows not spread)
        const animateOverviewMode2 = opt.OVERVIEW_MODE2 && !(finalState === 1 && opt.WORKSPACE_MODE);
        if (!Main.layoutManager._startingUp && ((!opt.SHOW_WS_PREVIEW_BG && !(opt.OVERVIEW_MODE2)) || animateOverviewMode2)) {
            if (!tmbBox._translationOriginal || Math.abs(tmbBox._translationOriginal[0]) > 500) { // swipe gesture can call this calculation before tmbBox is finalized, giving nonsense width
                const [tmbTranslation_x, tmbTranslation_y, dashTranslation_x, dashTranslation_y, searchTranslation_y] = _Util.getOverviewTranslations(opt, dash, tmbBox, searchEntryBin);
                tmbBox._translationOriginal = [tmbTranslation_x, tmbTranslation_y];
                dash._translationOriginal = [dashTranslation_x, dashTranslation_y];
                searchEntryBin._translationOriginal = searchTranslation_y;
            }
            if (finalState === 0 || initialState === 0) {
                const prg = Math.abs((finalState == 0 ? 0 : 1) - progress);
                tmbBox.translation_x = Math.round(prg * tmbBox._translationOriginal[0]);
                tmbBox.translation_y = Math.round(prg * tmbBox._translationOriginal[1]);
                if (!skipDash) {
                    dash.translation_x = Math.round(prg * dash._translationOriginal[0]);
                    dash.translation_y = Math.round(prg * dash._translationOriginal[1]);
                }
                searchEntryBin.translation_y = Math.round(prg * searchEntryBin._translationOriginal);
            }
            if (progress === 1) {
                tmbBox._translationOriginal = 0;
                if (!skipDash) {
                    dash._translationOriginal = 0;
                }
                searchEntryBin._translationOriginal = 0;
            }
        } else if (!Main.layoutManager._startingUp && (tmbBox.translation_x || tmbBox.translation_y)) {
            tmbBox.translation_x = 0;
            tmbBox.translation_y = 0;
            if (!skipDash) {
                dash.translation_x = 0;
                dash.translation_y = 0;
            }
            searchEntryBin.translation_y = 0;
        }

        if (!Main.layoutManager._startingUp) {
            if (initialState === ControlsState.HIDDEN && finalState === ControlsState.APP_GRID) {
                this._appDisplay.opacity = Math.round(progress * 255);
            } else {
                this._appDisplay.opacity = 255 - opacity;
            }
        }

        if (currentState === ControlsState.APP_GRID) {
            // in app grid hide workspaces so they're not blocking app grid or ws thumbnails
            this._workspacesDisplay.scale_x = 0;
        } else {
            this._workspacesDisplay.scale_x = 1;
        }
        this._workspacesDisplay.setPrimaryWorkspaceVisible(workspacesDisplayVisible);

        if (!this.dash._isAbove && progress > 0 && opt.OVERVIEW_MODE2) {
            // set searchEntry above appDisplay
            this.set_child_above_sibling(this._searchEntryBin, null);
            // move dash above wsTmb for case that dash and wsTmb animate from the same side
            this.set_child_above_sibling(dash, null);
            this.set_child_below_sibling(this._thumbnailsBox, null);
            this.set_child_below_sibling(this._workspacesDisplay, null);
            this.set_child_below_sibling(this._appDisplay, null);
        } else if (!this.dash._isAbove && progress === 1 && finalState > ControlsState.HIDDEN) {
            // set dash above workspace in the overview
            if (!_dashNotDefault()) {
                this.set_child_above_sibling(this._thumbnailsBox, null);
                this.set_child_above_sibling(this._searchEntryBin, null);
                this.set_child_above_sibling(this.dash, null);
                this.dash._isAbove = true;
            }

            // update max tmb scale in case some other extension changed it
            WorkspaceThumbnail.MAX_THUMBNAIL_SCALE = opt.MAX_THUMBNAIL_SCALE;

        } else if (this.dash._isAbove && progress < 1) {
            // keep dash below for ws transition between the overview and hidden state
            this.set_child_above_sibling(this._workspacesDisplay, null);
            this.dash._isAbove = false;
        }
    },

    // fix for upstream bug - appGrid.visible after transition from APP_GRID to HIDDEN
    _updateAppDisplayVisibility: function(stateTransitionParams = null) {
        if (!stateTransitionParams)
            stateTransitionParams = this._stateAdjustment.getStateTransitionParams();

        const { currentState } = stateTransitionParams;
        if (this.dash.showAppsButton.checked)
            this._searchTransition = false;

        // if !APP_GRID_ANIMATION, appGrid needs to be hidden in WINDOW_PICKER mode (1)
        // but needs to be visible for transition from HIDDEN (0) to APP_GRID (2)
        this._appDisplay.visible =
            currentState > ControlsState.HIDDEN &&
            !this._searchController.searchActive &&
            !(currentState === ControlsState.WINDOW_PICKER && !opt.APP_GRID_ANIMATION) &&
            !this._searchTransition;
    },

    _onSearchChanged: function() {
        // if user start typing or activated search provider during overview animation, this switcher will be called again after animation ends
        if (opt.SEARCH_VIEW_ANIMATION && Main.overview._animationInProgress) return;

        const { finalState, currentState } = this._stateAdjustment.getStateTransitionParams();
        const { searchActive } = this._searchController;
        const SIDE_CONTROLS_ANIMATION_TIME = 250; // OverviewControls.SIDE_CONTROLS_ANIMATION_TIME = Overview.ANIMATION_TIME = 250

        if (!searchActive) {
            this._workspacesDisplay.reactive = true;
            this._workspacesDisplay.setPrimaryWorkspaceVisible(true);
        } else {
            this._searchController.show();
        }

        this._searchTransition = true;

        this._searchController._searchResults.translation_x = 0;
        this._searchController._searchResults.translation_y = 0;
        this._searchController.visible = true;

        if (opt.SEARCH_VIEW_ANIMATION && !this.dash.showAppsButton.checked && ![4, 8].includes(opt.WS_TMB_POSITION) /*&& !opt.OVERVIEW_MODE2*/) {
            this._updateAppDisplayVisibility();

            this._searchController.opacity = searchActive ? 255 : 0;
            let translation_x = 0;
            let translation_y = 0;
            const geometry = global.display.get_monitor_geometry(global.display.get_primary_monitor());


            if (currentState < ControlsState.APP_GRID) {
                switch (opt.SEARCH_VIEW_ANIMATION) {
                    case 0:
                        translation_x = 0;
                        translation_y = 0;
                        break;
                    case 1:
                        // make it longer to cover the delay before results appears
                        translation_x = geometry.x + geometry.width - this._searchController.x + this._workspacesDisplay.width;
                        translation_y = 0;
                        break;
                    case 2:
                        translation_x = - this._searchController.x - 2 * this._workspacesDisplay.width;
                        translation_y = 0;
                        break;
                    case 3:
                        translation_x = 0;
                        translation_y = geometry.y + geometry.height + this._searchController.y + this._workspacesDisplay.height;
                        break;
                    case 5:
                        translation_x = 0;
                        translation_y = - this._searchController.y - 2 * this._workspacesDisplay.height;
                        break;
                }
            }

            if (searchActive) {
                this._searchController._searchResults.translation_x = translation_x;
                this._searchController._searchResults.translation_y = translation_y;
            } else {
                this._searchController._searchResults.translation_x = 0;
                this._searchController._searchResults.translation_y = 0;
            }

            this._searchController._searchResults.ease({
                //opacity: searchActive ? 255 : 0,
                translation_x: searchActive ? 0 : translation_x,
                translation_y: searchActive ? 0 : translation_y,
                duration: SIDE_CONTROLS_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => {
                    this._searchController.visible = searchActive;
                    this._searchTransition = false;
                }
            });

            this._workspacesDisplay.opacity = 255;
        } else {
            this._appDisplay.ease({
                opacity: (searchActive || currentState < 2) ? 0 : 255,
                duration: SIDE_CONTROLS_ANIMATION_TIME / 2,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => this._updateAppDisplayVisibility(),
            });

            //this._updateAppDisplayVisibility();
            this._workspacesDisplay.setPrimaryWorkspaceVisible(true);
            this._workspacesDisplay.ease({
                opacity: searchActive ? 0 : 255,
                duration: SIDE_CONTROLS_ANIMATION_TIME / 2,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => {
                    this._workspacesDisplay.reactive = !searchActive;
                    this._workspacesDisplay.setPrimaryWorkspaceVisible(!searchActive);
                },
            });

            this._searchController.ease({
                opacity: searchActive ? 255 : 0,
                duration: SIDE_CONTROLS_ANIMATION_TIME / 2,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => (this._searchController.visible = searchActive),
            });
        }

        // reuse already tuned overview transition, just replace APP_GRID with the search view
        if (!Main.overview._animationInProgress && finalState !== ControlsState.HIDDEN && !this.dash.showAppsButton.checked) {
            this._stateAdjustment.ease(searchActive ? ControlsState.APP_GRID : ControlsState.WINDOW_PICKER, {
                // shorter animation time when entering search view can avoid stuttering in transition
                // collecting search results take some time and the problematic part is the realization of the object on the screen
                // if the ws animation ends before this event, the whole transition is smoother
                // removing the ws transition (duration: 0) seems like the best solution here
                duration: searchActive ? 0 : SIDE_CONTROLS_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => {
                    this._workspacesDisplay.setPrimaryWorkspaceVisible(!searchActive);
                }
            });
        }

        const entry = this._searchEntry;
        if (opt.SHOW_SEARCH_ENTRY) {
            entry.visible = true;
            entry.opacity = 255;
        } else {
            entry.visible = true;
            entry.opacity = searchActive ? 0 : 255;
            // show search entry only if the user starts typing, and hide it when leaving the search mode
            entry.ease({
                opacity: searchActive ? 255 : 0,
                duration: SIDE_CONTROLS_ANIMATION_TIME / 2,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => {
                    entry.visible = searchActive;
                },
            });
        }
    },

    runStartupAnimation: async function(callback) {
        // fix for upstream bug - overview always shows workspace 1 instead of the active one after restart
        this._workspaceAdjustment.set_value(global.workspace_manager.get_active_workspace_index());
        this._ignoreShowAppsButtonToggle = true;

        this._searchController.prepareToEnterOverview();
        this._workspacesDisplay.prepareToEnterOverview();

        this._stateAdjustment.value = ControlsState.HIDDEN;
        this._stateAdjustment.ease(ControlsState.WINDOW_PICKER, {
            duration: ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });

        this.dash.showAppsButton.checked = false;
        this._ignoreShowAppsButtonToggle = false;

        // Set the opacity here to avoid a 1-frame flicker
        this.opacity = 0;

        // We can't run the animation before the first allocation happens
        await this.layout_manager.ensureAllocation();

        const { STARTUP_ANIMATION_TIME } = imports.ui.layout;

        // Opacity
        this.ease({
            opacity: 255,
            duration: STARTUP_ANIMATION_TIME,
            mode: Clutter.AnimationMode.LINEAR,
            onComplete: () => {
                // part of the workaround for stuttering first app grid animation
                this._appDisplay.visible = true;
            }
        });

        const dash = this.dash;
        const tmbBox = this._thumbnailsBox;

        // Set the opacity here to avoid a 1-frame flicker
        dash.opacity = 0;
        for (const view of this._workspacesDisplay._workspacesViews) {
            if (view._monitorIndex !== global.display.get_primary_monitor())
                view._thumbnails.opacity = 0;
        }

        const searchEntryBin = this._searchEntryBin;
        const [tmbTranslation_x, tmbTranslation_y, dashTranslation_x, dashTranslation_y, searchTranslation_y] =
            _Util.getOverviewTranslations(opt, dash, tmbBox, searchEntryBin);

        const onComplete = function() {
            // running init callback again causes issues (multiple connections)
            if (!_startupInitComplete)
                callback();
            _startupInitComplete = true;

            // force app grid to build before the first visible animation to remove possible stuttering
            this._appDisplay.opacity = 1;

            const [x, y] = this._appDisplay.get_position();
            const translation_x = - x;
            const translation_y = - y;
            this._appDisplay.translation_x = translation_x;
            this._appDisplay.translation_y = translation_y;

            // let the main loop realize previous changes before continuing
            _startupAnimTimeoutId1 = GLib.timeout_add(
                GLib.PRIORITY_DEFAULT,
                10,
                () => {
                    this._appDisplay.translation_x = 0;
                    this._appDisplay.translation_y = 0;
                    this._appDisplay.visible = false;
                    if (opt.STARTUP_STATE === 1) {
                        Main.overview.hide();
                    } else if (opt.STARTUP_STATE === 2) {
                        this._appDisplay.opacity = 255;
                        this.dash.showAppsButton.checked = true;
                    }
                    _startupAnimTimeoutId1 = 0;
                    return GLib.SOURCE_REMOVE;
                }
            );
        }.bind(this);

        if (dash.visible && !_dashNotDefault()) {
            dash.translation_x = dashTranslation_x;
            dash.translation_y = dashTranslation_y;
            dash.opacity = 255;
            dash.ease({
                translation_x: 0,
                translation_y: 0,
                delay: STARTUP_ANIMATION_TIME / 2,
                duration: STARTUP_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => {
                    onComplete();
                },
            });
        } else {
            // set dash opacity to make it visible if user enable it later
            dash.opacity = 255;
            // if dash is hidden, substitute the ease timeout with GLib.timeout
            _startupAnimTimeoutId2 = GLib.timeout_add(
                GLib.PRIORITY_DEFAULT,
                // delay + animation time
                STARTUP_ANIMATION_TIME * 2 * opt.ANIMATION_TIME_FACTOR,
                () => {
                    onComplete();
                    _startupAnimTimeoutId2 = 0;
                    return GLib.SOURCE_REMOVE;
                }
            );
        }

        if (searchEntryBin.visible) {
            searchEntryBin.translation_y = searchTranslation_y;
            searchEntryBin.ease({
                translation_y: 0,
                delay: STARTUP_ANIMATION_TIME / 2,
                duration: STARTUP_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        }

        if (tmbBox.visible) {
            tmbBox.translation_x = tmbTranslation_x;
            tmbBox.translation_y = tmbTranslation_y;
            tmbBox.ease({
                translation_x: 0,
                translation_y: 0,
                delay: STARTUP_ANIMATION_TIME / 2,
                duration: STARTUP_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        }

        // upstream bug - following animation will be cancelled, don't know where
        // needs further investigation
        const  workspacesViews = this._workspacesDisplay._workspacesViews;
        if (workspacesViews.length > 1) {
            for (const view of workspacesViews) {
                if (view._monitorIndex !== global.display.get_primary_monitor() && view._thumbnails.visible) {
                    const tmbBox = view._thumbnails;

                    _Util.getOverviewTranslations(opt, dash, tmbBox, searchEntryBin);
                    if (opt.SEC_WS_TMB_LEFT) {
                        tmbBox.translation_x = - (tmbBox.width + 12); // compensate for padding
                    } else if (opt.SEC_WS_TMB_RIGHT) {
                        tmbBox.translation_x = (tmbBox.width + 12);
                    } else if (opt.SEC_WS_TMB_TOP) {
                        tmbBox.translation_y = - (tmbBox.height + 12);
                    } else if (opt.SEC_WS_TMB_BOTTOM) {
                        tmbBox.translation_y = (tmbBox.height + 12);
                    }
                    tmbBox.opacity = 255;

                    tmbBox.ease({
                        translation_y: 0,
                        delay: STARTUP_ANIMATION_TIME / 2,
                        duration: STARTUP_ANIMATION_TIME,
                        mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                    });
                }
            }
        }
    },

    animateToOverview: function(state, callback) {
        // don't enter overview during updating appDisplay properties
        if (_updateAppGridTimeoutId)
            Main.overview.hide();
        this._ignoreShowAppsButtonToggle = true;
        this._searchTransition = false;

        this._searchController.prepareToEnterOverview();
        this._workspacesDisplay.prepareToEnterOverview();

        this._stateAdjustment.value = ControlsState.HIDDEN;

        // building window thumbnails takes some time and with many windows on the workspace
        // the time can be close to or longer than ANIMATION_TIME
        // in which case the the animation is greatly delayed, stuttering, or even skipped
        // for user it is more acceptable to watch delayed smooth animation,
        // even if it takes little more time, than jumping frames
        const delay = global.display.get_tab_list(0, global.workspace_manager.get_active_workspace()).length * 3;
        this._stateAdjustment.ease(state, {
            delay,
            duration: 250, //Overview.ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onStopped: () => {
                if (callback)
                    callback();
            },
        });

        this.dash.showAppsButton.checked =
            state === ControlsState.APP_GRID;

        this._ignoreShowAppsButtonToggle = false;
    },
}

var ControlsManagerLayoutVertical = {
    _computeWorkspacesBoxForState: function(state, box, workAreaBox, dashWidth, dashHeight, thumbnailsWidth, searchHeight, startY) {
        const workspaceBox = box.copy();
        let [width, height] = workspaceBox.get_size();
        const { x1: startX,/* y1: startY*/ } = workAreaBox;
        const { spacing } = this;
        //const { expandFraction } = this._workspacesThumbnails;

        const dash = Main.overview.dash;
        // including Dash to Dock and clones properties for compatibility

        if (_dashIsDashToDock()) {
            // Dash to Dock also always affects workAreaBox
            Main.layoutManager._trackedActors.forEach((actor) => {
                if (actor.affectsStruts && actor.actor.width === dash.width) {
                    if (dash._isHorizontal) {
                        // disabled inteli-hide don't needs compensation
                        // startY needs to be corrected in allocate()
                        if (dash.get_parent()?.get_parent()?.get_parent()?._intellihideIsEnabled) {
                            height += dash.height;
                        }
                    } else {
                        width += dash.width;
                    }
                }
            });
        }

        let wWidth;
        let wHeight;
        let wsBoxY;

        switch (state) {
        case ControlsState.HIDDEN:
            // if PANEL_MODE == 2 (overview only) the affectStruts property stays on false to avoid stuttering
            // therefore we added panel height to startY for the overview allocation,
            // but here we need to remove the correction since the panel will be in the hidden state
            if (opt.START_Y_OFFSET) {
                let [x, y] = workAreaBox.get_origin();
                y -= opt.START_Y_OFFSET;
                workspaceBox.set_origin(x, y);
            } else {
                workspaceBox.set_origin(...workAreaBox.get_origin());
            }
            workspaceBox.set_size(...workAreaBox.get_size());
            break;
        case ControlsState.WINDOW_PICKER:
        case ControlsState.APP_GRID:
            if (opt.WS_ANIMATION && opt.SHOW_WS_TMB && state === ControlsState.APP_GRID) {
                workspaceBox.set_origin(...this._workspacesThumbnails.get_position());
                workspaceBox.set_size(...this._workspacesThumbnails.get_size());
            } else if (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE) {
                if (opt.START_Y_OFFSET) {
                    let [x, y] = workAreaBox.get_origin();
                    y -= opt.START_Y_OFFSET;
                    workspaceBox.set_origin(x, y);
                } else {
                    workspaceBox.set_origin(...workAreaBox.get_origin());
                }
                workspaceBox.set_size(...workAreaBox.get_size());
            } else {
                // in PANEL_MODE 2 panel don't affects workArea height (affectStruts === false), it needs to be compensated
                height = opt.PANEL_POSITION_TOP ? height : height - Main.panel.height;
                searchHeight = opt.SHOW_SEARCH_ENTRY ? searchHeight : 0;
                wWidth = width
                            - (opt.DASH_VERTICAL ? dash.width : 0)
                            - (thumbnailsWidth)
                            - 4 * spacing
                wHeight = height
                            - (opt.DASH_VERTICAL ? 0 : dashHeight)
                            - searchHeight
                            - 4 * spacing;

                const ratio = width / height;
                let wRatio = wWidth / wHeight;
                let scale = ratio / wRatio;

                if (scale > 1) {
                    wHeight = wHeight / scale;
                    wWidth = wHeight * ratio;
                } else {
                    wWidth = wWidth * scale;
                    wHeight = wWidth / ratio;
                }

                // height decides the actual size, ratio is given by the workarea
                wHeight *= opt.WS_PREVIEW_SCALE;
                wWidth *= opt.WS_PREVIEW_SCALE;

                let xOffset = 0;
                let yOffset = 0;

                const yOffsetT = (opt.DASH_TOP ? dashHeight : 0) + searchHeight;
                const yOffsetB = (opt.DASH_BOTTOM ? dashHeight : 0);
                const yAvailableSpace = (height - yOffsetT - wHeight - yOffsetB) / 2;
                yOffset = yOffsetT + yAvailableSpace;

                const centeredBoxX = (width - wWidth) / 2;

                const xOffsetL = (opt.DASH_LEFT ? dashWidth : 0) + (opt.WS_TMB_LEFT ? thumbnailsWidth : 0) + 2 * spacing;
                const xOffsetR = (opt.DASH_RIGHT ? dashWidth : 0) + (opt.WS_TMB_RIGHT ? thumbnailsWidth : 0) + 2 * spacing;

                this._xAlignCenter = false;
                if (centeredBoxX < Math.max(xOffsetL, xOffsetR)) {
                    xOffset = xOffsetL + spacing + (width - xOffsetL - wWidth - xOffsetR - 2 * spacing) / 2;
                } else {
                    xOffset = centeredBoxX;
                    this._xAlignCenter = true;
                }

                const wsBoxX = /*startX + */xOffset;
                wsBoxY = Math.round(startY + yOffset);
                workspaceBox.set_origin(Math.round(wsBoxX), Math.round(wsBoxY));
                workspaceBox.set_size(Math.round(wWidth), Math.round(wHeight));
            }
        }

        return workspaceBox;
    },

    _getAppDisplayBoxForState: function(state, box, workAreaBox, searchHeight, dashWidth, dashHeight, thumbnailsWidth, startY) {
        const [width] = box.get_size();
        const { x1: startX } = workAreaBox;
        //const { y1: startY } = workAreaBox;
        let height = workAreaBox.get_height();
        // in PANEL_MODE 2 panel don't affects workArea height (affectStruts === false), it needs to be compensated
        height = opt.PANEL_MODE === 2 ? height - Main.panel.height : height;
        const appDisplayBox = new Clutter.ActorBox();
        const { spacing } = this;

        searchHeight = opt.SHOW_SEARCH_ENTRY ? searchHeight : 0;

        const xOffsetL = (opt.WS_TMB_LEFT ? thumbnailsWidth : 0) + (opt.DASH_LEFT ? dashWidth : 0);
        const xOffsetR = (opt.WS_TMB_RIGHT ? thumbnailsWidth : 0) + (opt.DASH_RIGHT ? dashWidth : 0);
        const yOffsetT = (opt.DASH_TOP ? dashHeight : 0) + (opt.SHOW_SEARCH_ENTRY ? searchHeight : 0);
        const yOffsetB = (opt.DASH_BOTTOM ? dashHeight : 0);
        const adWidth = opt.CENTER_APP_GRID ? (width - 2 * Math.max (xOffsetL, xOffsetR) - 4 * spacing) : (width - xOffsetL - xOffsetR - 4 * spacing);
        const adHeight = height - yOffsetT - yOffsetB - 4 * spacing;

        const appDisplayX = opt.CENTER_APP_GRID ? ((width - adWidth) / 2) : (xOffsetL + 2 * spacing);
        const appDisplayY = startY + yOffsetT + 2 * spacing;

        switch (state) {
        case ControlsState.HIDDEN:
        case ControlsState.WINDOW_PICKER:
            // 1 - left, 2 - right, 3 - bottom, 5 - top
            switch (opt.APP_GRID_ANIMATION) {
            case 0:
                appDisplayBox.set_origin(appDisplayX, appDisplayY);
                break;
            case 1:
                appDisplayBox.set_origin(startX + width, appDisplayY);
                break;
            case 2:
                appDisplayBox.set_origin(startX - adWidth, appDisplayY);
                break;
            case 3:
                appDisplayBox.set_origin(appDisplayX, workAreaBox.y2);
                break;
            case 5:
                appDisplayBox.set_origin(appDisplayX, workAreaBox.y1 - adHeight);
                break;
            }
            break;
        case ControlsState.APP_GRID:
            appDisplayBox.set_origin(appDisplayX, appDisplayY);
            break;
        }

        appDisplayBox.set_size(adWidth, adHeight);
        return appDisplayBox;
    },

    vfunc_allocate: function(container, box) {
        const childBox = new Clutter.ActorBox();

        const { spacing } = this;

        const monitor = Main.layoutManager.findMonitorForActor(this._container);
        const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor.index);
        const startX = workArea.x - monitor.x;
        // if PANEL_MODE == 2 (overview only) the affectStruts property stays on false to avoid stuttering
        // therefore we need to add panel height to startY
        let startY = workArea.y - monitor.y + opt.START_Y_OFFSET;

        const workAreaBox = new Clutter.ActorBox();
        workAreaBox.set_origin(startX, startY);
        workAreaBox.set_size(workArea.width, workArea.height);
        box.y1 += startY;
        box.x1 += startX;
        let [width, height] = box.get_size();
        // if panel is at bottom position,
        // compensate the height of the available box (the box size is calculated for top panel)
        height = opt.PANEL_POSITION_TOP ? height : height - Main.panel.height;
        let availableHeight = height;

        // Dash
        const maxDashHeight = Math.round(box.get_height() * DASH_MAX_SIZE_RATIO);
        const maxDashWidth = maxDashHeight * 0.8;
        let dashHeight = 0;
        let dashWidth = 0;

        // dash cloud be overridden by the Dash to Dock clone
        const dash = Main.overview.dash;
        if (_dashIsDashToDock()) {
            // if Dash to Dock replaced the default dash and its inteli-hide id disabled we need to compensate for affected startY
            if (!Main.overview.dash.get_parent()?.get_parent()?.get_parent()?._intellihideIsEnabled) {
                if (Main.panel.y === monitor.y)
                    startY = Main.panel.height + spacing;
            }
            dashHeight = dash.height;
            dashWidth = dash.width;
            opt.DASH_VERTICAL = [1, 3].includes(dash._position);
            this._dash.allocate(childBox);
        } else if (this._dash.visible) {
            // default dock
            if (opt.DASH_VERTICAL) {
                this._dash.setMaxSize(maxDashWidth, height);
                [, dashWidth] = this._dash.get_preferred_width(height);
                [, dashHeight] = this._dash.get_preferred_height(dashWidth);
                dashWidth = Math.min(dashWidth, maxDashWidth);
                dashHeight = Math.min(dashHeight, height);

            } else if (!opt.WS_TMB_FULL) {
                    this._dash.setMaxSize(width, maxDashHeight);
                [, dashHeight] = this._dash.get_preferred_height(width);
                [, dashWidth] = this._dash.get_preferred_width(dashHeight);
                dashHeight = Math.min(dashHeight, maxDashHeight);
                dashWidth = Math.min(dashWidth, width);
            }
        }

        // Workspace Thumbnails
        let wsTmbWidth = 0;
        let wsTmbHeight = 0;

        if (this._workspacesThumbnails.visible) {
            //const REDUCE_WS_TMB_IF_NEEDED = (this._searchController._searchActive && opt.CENTER_SEARCH_VIEW) || opt.CENTER_APP_GRID;

            const { expandFraction } = this._workspacesThumbnails;
            const dashHeightReservation = (!opt.WS_TMB_FULL && !opt.DASH_VERTICAL) ? dashHeight : 0;
            wsTmbHeight = opt.WS_TMB_FULL
                                ? height - spacing
                                : height - (opt.DASH_VERTICAL ? 0 : dashHeightReservation) - spacing;

            wsTmbWidth = this._workspacesThumbnails.get_preferred_custom_width(wsTmbHeight)[0];
            wsTmbWidth = Math.round(Math.min(
                wsTmbWidth * expandFraction,
                width * opt.MAX_THUMBNAIL_SCALE
            ));

            /*if (opt.REDUCE_WS_TMB_IF_NEEDED) {
                const searchAllocation = this._searchController._searchResults._content.allocation;
                const searchWidth = searchAllocation.x2 - searchAllocation.x1;
                wsTmbWidth = Math.clamp((width - searchWidth) / 2 - spacing, width * 0.05, wsTmbWidth);
            }*/

            wsTmbHeight = Math.round(Math.min(this._workspacesThumbnails.get_preferred_custom_height(wsTmbWidth)[1], wsTmbHeight));

            let wsTmbX;
            if (opt.WS_TMB_RIGHT) {
                wsTmbX = Math.round(startX + width - (opt.DASH_RIGHT ? dashWidth : 0) - wsTmbWidth - spacing / 2);
            } else {
                wsTmbX = Math.round((opt.DASH_LEFT ? dashWidth : 0) + spacing / 2);
            }

            let wstOffset = (height - wsTmbHeight - (opt.DASH_VERTICAL ? 0 : dashHeightReservation)) / 2;
            wstOffset = wstOffset - opt.WS_TMB_POSITION_ADJUSTMENT * (wstOffset - spacing / 2);
            let wsTmbY = Math.round(startY + ((dashHeightReservation && opt.DASH_TOP) ? dashHeight : 0) + wstOffset);

            childBox.set_origin(wsTmbX, wsTmbY);
            childBox.set_size(wsTmbWidth, wsTmbHeight);

            this._workspacesThumbnails.allocate(childBox);
        }


        if (this._dash.visible) {
            const wMaxWidth = width - spacing - wsTmbWidth - 2 * spacing - (opt.DASH_VERTICAL ? dashWidth + spacing : 0);
            if (opt.WS_TMB_FULL && !opt.DASH_VERTICAL) {
                this._dash.setMaxSize(wMaxWidth, maxDashHeight);
                [, dashHeight] = this._dash.get_preferred_height(wMaxWidth);
                [, dashWidth] = this._dash.get_preferred_width(dashHeight);
                dashHeight = Math.round(Math.min(dashHeight, maxDashHeight));
                dashWidth = Math.round(Math.min(dashWidth, wMaxWidth));
            }

            let dashX, dashY, offset;
            if (opt.DASH_RIGHT)
                dashX = width - dashWidth;
            else if (opt.DASH_LEFT) {
                dashX = 0;
            }
            else if (opt.DASH_TOP)
                dashY = startY;
            else
                dashY = startY + height - dashHeight;

            if (!opt.DASH_VERTICAL) {
                offset = (width - (((opt.WS_TMB_FULL || opt.CENTER_DASH_WS) && !this._xAlignCenter) ? wsTmbWidth : 0) - dashWidth) / 2;
                offset = offset - opt.DASH_POSITION_ADJUSTMENT * (offset - spacing / 2);
                dashX = offset;

                if ((opt.WS_TMB_FULL || opt.CENTER_DASH_WS) && !this._xAlignCenter) {
                    if (opt.WS_TMB_RIGHT) {
                        //dashX = Math.min(dashX, width - dashWidth - (wsTmbWidth ? wsTmbWidth : 0));
                    } else {
                        dashX = (wsTmbWidth ? wsTmbWidth : 0) + offset;
                        dashX = Math.max(dashX, wsTmbWidth ? wsTmbWidth + spacing : 0);
                        dashX = Math.min(dashX, width - dashWidth - spacing);
                    }
                }
                if (opt.WS_TMB_FULL && !opt.CENTER_DASH_WS) {
                    dashX = opt.WS_TMB_RIGHT
                                ? Math.min(width - wsTmbWidth - dashWidth, dashX + (wsTmbWidth) / 2 * (1 - Math.abs(opt.DASH_POSITION_ADJUSTMENT)))
                                : Math.max(wsTmbWidth, dashX - (wsTmbWidth) / 2 * (1 - Math.abs(opt.DASH_POSITION_ADJUSTMENT)));
                }
            } else {
                const offset = (height - dashHeight) / 2;
                dashY = startY + (offset - opt.DASH_POSITION_ADJUSTMENT * offset);
            }

            childBox.set_origin(Math.round(startX + dashX), Math.round(dashY));
            childBox.set_size(dashWidth, dashHeight);
            this._dash.allocate(childBox);
        }

        availableHeight -= (opt.DASH_VERTICAL ? 0 : dashHeight + spacing);

        let [searchHeight] = this._searchEntry.get_preferred_height(width - wsTmbWidth);

        // Workspaces
        let params = [box, workAreaBox, dashWidth, dashHeight, wsTmbWidth, searchHeight, startY];
        const transitionParams = this._stateAdjustment.getStateTransitionParams();

        // Update cached boxes
        for (const state of Object.values(ControlsState)) {
            this._cachedWorkspaceBoxes.set(
                state, this._computeWorkspacesBoxForState(state, ...params));
        }

        let workspacesBox;
        if (!transitionParams.transitioning) {
            workspacesBox = this._cachedWorkspaceBoxes.get(transitionParams.currentState);
        }
        if (!workspacesBox) {
            const initialBox = this._cachedWorkspaceBoxes.get(transitionParams.initialState);
            const finalBox = this._cachedWorkspaceBoxes.get(transitionParams.finalState);
            workspacesBox = initialBox.interpolate(finalBox, transitionParams.progress);
        }

        this._workspacesDisplay.allocate(workspacesBox);

        // Search entry
        const searchXoffset = (opt.DASH_LEFT ? dashWidth : 0) + spacing + (opt.WS_TMB_RIGHT ? 0 : wsTmbWidth + spacing);
        //let [searchHeight] = this._searchEntry.get_preferred_height(width - wsTmbWidth);

        // Y position under top Dash
        let searchEntryX, searchEntryY;
        /*if (opt.OVERVIEW_MODE2 && !opt.DASH_TOP && !opt.WS_TMB_TOP) {
            searchEntryY = 7;
        } else*/ if (opt.DASH_TOP) {
            searchEntryY = startY + dashHeight - spacing;
        } else {
            searchEntryY = startY;
        }

        searchEntryX = searchXoffset;
        let searchWidth = width - 2 * spacing - wsTmbWidth - (opt.DASH_VERTICAL ? dashWidth : 0); // xAlignCenter is given by wsBox
        searchWidth = this._xAlignCenter ? width - 2 * (wsTmbWidth + spacing) : searchWidth;

        if (opt.CENTER_SEARCH_VIEW) {
            childBox.set_origin(0, searchEntryY);
            childBox.set_size(width, searchHeight);
        } else {
            childBox.set_origin(this._xAlignCenter ? 0 : searchEntryX, searchEntryY);
            childBox.set_size(this._xAlignCenter ? width : searchWidth - spacing, searchHeight);
        }

        this._searchEntry.allocate(childBox);

        availableHeight -= searchHeight + spacing;

        // AppDisplay - state, box, workAreaBox, searchHeight, dashHeight, appGridBox, wsTmbWidth
        //if (this._appDisplay.visible) {

            params = [box, workAreaBox, searchHeight, dashWidth, dashHeight, wsTmbWidth, startY]; // send startY, can be compensated
            let appDisplayBox;
            if (!transitionParams.transitioning) {
                appDisplayBox =
                    this._getAppDisplayBoxForState(transitionParams.currentState, ...params);
            } else {
                const initialBox =
                    this._getAppDisplayBoxForState(transitionParams.initialState, ...params);
                const finalBox =
                    this._getAppDisplayBoxForState(transitionParams.finalState, ...params);

                appDisplayBox = initialBox.interpolate(finalBox, transitionParams.progress);
            }
            this._appDisplay.allocate(appDisplayBox);
        //}

        // Search
        if (opt.CENTER_SEARCH_VIEW) {
            const dashW = (opt.DASH_VERTICAL ? dashWidth : 0) + spacing;
            searchWidth = width - 2 * wsTmbWidth - 2 * dashW;
            childBox.set_origin(wsTmbWidth + dashW, startY + (opt.DASH_TOP ? dashHeight : spacing) + searchHeight);
        } else {
            childBox.set_origin(this._xAlignCenter ? wsTmbWidth + spacing : searchXoffset, startY + (opt.DASH_TOP ? dashHeight : spacing) + searchHeight);
        }

        childBox.set_size(searchWidth, availableHeight);
        this._searchController.allocate(childBox);

        this._runPostAllocation();
    }
}

var ControlsManagerLayoutHorizontal = {
    _computeWorkspacesBoxForState: function(state, box, workAreaBox, dashWidth, dashHeight, thumbnailsHeight, searchHeight, startY) {
        const workspaceBox = box.copy();
        let [width, height] = workspaceBox.get_size();
        let { x1: startX/*, y1: startY*/ } = workAreaBox;
        const { spacing } = this;
        //const { expandFraction } = this._workspacesThumbnails;

        const dash = Main.overview.dash;
        // including Dash to Dock and clones properties for compatibility
        if (_dashIsDashToDock()) {
            // Dash to Dock always affects workAreaBox
            Main.layoutManager._trackedActors.forEach((actor) => {
                if (actor.affectsStruts && actor.actor.width === dash.width) {
                    if (dash._isHorizontal) {
                        // disabled inteli-hide don't need compensation
                        // startY needs to be corrected in allocate()
                        if (dash.get_parent()?.get_parent()?.get_parent()?._intellihideIsEnabled) {
                            height += dash.height;
                        } else if (opt.DASH_TOP) {
                            height += dash.height;
                        }
                    } else {
                        width += dash.width;
                    }
                }
            });
        }

        let wWidth, wHeight, wsBoxY, wsBoxX;

        switch (state) {
        case ControlsState.HIDDEN:
            // if PANEL_MODE == 2 (overview only) the affectStruts property stays on false to avoid stuttering
            // therefore we added panel height to startY for the overview allocation,
            // but here we need to remove the correction since the panel will be in the hidden state
            if (opt.START_Y_OFFSET) {
                let [x, y] = workAreaBox.get_origin();
                y -= opt.START_Y_OFFSET;
                workspaceBox.set_origin(x, y);
            } else {
                workspaceBox.set_origin(...workAreaBox.get_origin());
            }
            workspaceBox.set_size(...workAreaBox.get_size());
            break;
        case ControlsState.WINDOW_PICKER:
        case ControlsState.APP_GRID:
            if (opt.WS_ANIMATION && opt.SHOW_WS_TMB && state === ControlsState.APP_GRID) {
                workspaceBox.set_origin(...this._workspacesThumbnails.get_position());
                workspaceBox.set_size(...this._workspacesThumbnails.get_size());
            } else if (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE) {
                if (opt.START_Y_OFFSET) {
                    let [x, y] = workAreaBox.get_origin();
                    y -= opt.START_Y_OFFSET;
                    workspaceBox.set_origin(x, y);
                } else {
                    workspaceBox.set_origin(...workAreaBox.get_origin());
                }
                workspaceBox.set_size(...workAreaBox.get_size());
            } else {
                // in PANEL_MODE 2 panel don't affects workArea height (affectStruts === false), it needs to be compensated
                height = opt.PANEL_POSITION_TOP ? height : height - Main.panel.height;
                searchHeight = opt.SHOW_SEARCH_ENTRY ? searchHeight : 0;
                wWidth = width
                            - spacing
                            - (opt.DASH_VERTICAL ? dashWidth : 0)
                            - 4 * spacing;
                wHeight = height
                            - (opt.DASH_VERTICAL ? spacing : (dashHeight ? dashHeight : 0))
                            - (thumbnailsHeight ? thumbnailsHeight : 0)
                            - searchHeight
                            - 4 * spacing;

                const ratio = width / height;
                let wRatio = wWidth / wHeight;
                let scale = ratio / wRatio;

                if (scale > 1) {
                    wHeight = wHeight / scale;
                    wWidth = wHeight * ratio;
                } else {
                    wWidth = wWidth * scale;
                    wHeight = wWidth / ratio;
                }

                // height decides the actual size, ratio is given by the workarea
                wHeight *= opt.WS_PREVIEW_SCALE;
                wWidth *= opt.WS_PREVIEW_SCALE;

                let xOffset = 0;
                let yOffset = 0;

                const yOffsetT = (opt.DASH_TOP ? dashHeight : 0) + (opt.WS_TMB_TOP ? thumbnailsHeight : 0) + searchHeight;
                const yOffsetB = (opt.DASH_BOTTOM ? dashHeight : 0) + (opt.WS_TMB_BOTTOM ? thumbnailsHeight : 0);

                const yAvailableSpace = (height - yOffsetT - wHeight - yOffsetB) / 2;
                yOffset = yOffsetT + yAvailableSpace;

                const xOffsetL = (opt.DASH_LEFT ? dashWidth : 0) + spacing;
                const xOffsetR = (opt.DASH_RIGHT ? dashWidth : 0) + spacing;
                const centeredBoxX = (width - wWidth) / 2;

                this._xAlignCenter = false;
                if (centeredBoxX < Math.max(xOffsetL, xOffsetR)) {
                    xOffset = xOffsetL + spacing + (width - xOffsetL - wWidth - xOffsetR) / 2;
                } else {
                    xOffset = centeredBoxX;
                    this._xAlignCenter = true;
                }

                wsBoxX = /*startX + */xOffset;
                wsBoxY = Math.round(startY + yOffset)
                workspaceBox.set_origin(Math.round(wsBoxX), Math.round(wsBoxY));
                workspaceBox.set_size(Math.round(wWidth), Math.round(wHeight));
            }
        }

        return workspaceBox;
    },

    _getAppDisplayBoxForState: function(state, box, workAreaBox, searchHeight, dashWidth, dashHeight, thumbnailsHeight, startY) {
        const [width] = box.get_size();
        const { x1: startX } = workAreaBox;
        //const { y1: startY } = workAreaBox;
        let height = workAreaBox.get_height();
        // in PANEL_MODE 2 panel don't affects workArea height (affectStruts === false), it needs to be compensated
        height = opt.PANEL_MODE === 2 ? height - Main.panel.height : height;
        const appDisplayBox = new Clutter.ActorBox();
        const { spacing } = this;

        const yOffsetT = (opt.WS_TMB_TOP ? thumbnailsHeight : 0) + (opt.DASH_TOP ? dashHeight : 0) + (opt.SHOW_SEARCH_ENTRY ? searchHeight : 0);
        const yOffsetB = (opt.WS_TMB_BOTTOM ? thumbnailsHeight : 0) + (opt.DASH_BOTTOM ? dashHeight : 0);
        const xOffsetL = (opt.DASH_LEFT ? dashWidth : 0);
        const xOffsetR = (opt.DASH_RIGHT ? dashWidth : 0);
        const adWidth = opt.CENTER_APP_GRID ? (width - 2 * Math.max (xOffsetL, xOffsetR) - 4 * spacing) : (width - xOffsetL - xOffsetR - 4 * spacing);
        const adHeight = height - yOffsetT - yOffsetB - 4 * spacing;

        const appDisplayX = opt.CENTER_APP_GRID ? ((width - adWidth) / 2) : (xOffsetL + 2 * spacing);
        const appDisplayY = startY + yOffsetT + 2 * spacing;

        switch (state) {
        case ControlsState.HIDDEN:
        case ControlsState.WINDOW_PICKER:
            // 1 - left, 2 - right, 3 - bottom, 5 - top
            switch (opt.APP_GRID_ANIMATION) {
            case 0:
                appDisplayBox.set_origin(appDisplayX, appDisplayY);
                break;
            case 1:
                appDisplayBox.set_origin(startX + width, appDisplayY);
                break;
            case 2:
                appDisplayBox.set_origin(startX - adWidth, appDisplayY);
                break;
            case 3:
                appDisplayBox.set_origin(appDisplayX, workAreaBox.y2);
                break;
            case 5:
                appDisplayBox.set_origin(appDisplayX, workAreaBox.y1 - adHeight);
                break;
            }
            break;
        case ControlsState.APP_GRID:
            appDisplayBox.set_origin(appDisplayX, appDisplayY);
            break;
        }

        appDisplayBox.set_size(adWidth, adHeight);
        return appDisplayBox;
    },

    vfunc_allocate: function(container, box) {
        const childBox = new Clutter.ActorBox();

        const { spacing } = this;

        const monitor = Main.layoutManager.findMonitorForActor(this._container);
        const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor.index);
        const startX = workArea.x - monitor.x;
        // if PANEL_MODE == 2 (overview only) the affectStruts property stays on false to avoid stuttering
        // therefore we need to add panel height to startY
        let startY = workArea.y - monitor.y + opt.START_Y_OFFSET;
        const workAreaBox = new Clutter.ActorBox();
        workAreaBox.set_origin(startX, startY);
        workAreaBox.set_size(workArea.width, workArea.height);
        box.y1 += startY;
        box.x1 += startX;
        let [width, height] = box.get_size();
        // if panel is at bottom position,
        // compensate the height of the available box (the box size is calculated for top panel)
        height = opt.PANEL_POSITION_TOP ? height : height - Main.panel.height;
        let availableHeight = height;

        // Dash
        const maxDashHeight = Math.round(box.get_height() * DASH_MAX_SIZE_RATIO);
        const maxDashWidth = maxDashHeight * 0.8;
        let dashHeight = 0;
        let dashWidth = 0;

        // dash cloud be overridden by the Dash to Dock clone
        const dash = Main.overview.dash;
        if (_dashIsDashToDock()) {
            // if Dash to Dock replaced the default dash and its inteli-hide is disabled we need to compensate for affected startY
            if (!Main.overview.dash.get_parent()?.get_parent()?.get_parent()?._intellihideIsEnabled) {
                if (Main.panel.y === monitor.y)
                    startY = Main.panel.height + spacing;
            }
            dashHeight = dash.height;
            dashWidth = dash.width;
            opt.DASH_TOP = dash._position === 0;
            opt.DASH_VERTICAL = [1, 3].includes(dash._position);
            this._dash.allocate(childBox);
        } else if (this._dash.visible) {
            // default dock
            if (!opt.DASH_VERTICAL) {
                this._dash.setMaxSize(width, maxDashHeight);
                [, dashHeight] = this._dash.get_preferred_height(width);
                [, dashWidth] = this._dash.get_preferred_width(dashHeight);
                dashHeight = Math.min(dashHeight, maxDashHeight);
                dashWidth = Math.min(dashWidth, width - spacing);

            } else if (!opt.WS_TMB_FULL) {
                this._dash.setMaxSize(maxDashWidth, height);
                [, dashWidth] = this._dash.get_preferred_width(height);
                [, dashHeight] = this._dash.get_preferred_height(dashWidth);
                dashHeight = Math.min(dashHeight, height - spacing);
                dashWidth = Math.min(dashWidth, width);
            }
        }

        let [searchHeight] = this._searchEntry.get_preferred_height(width);

        let wsTmbWidth = 0;
        let wsTmbHeight = 0;

        if (this._workspacesThumbnails.visible) {
            const { expandFraction } = this._workspacesThumbnails;
            const dashWidthReservation = (!opt.WS_TMB_FULL && opt.DASH_VERTICAL) ? dashWidth : 0;

            wsTmbWidth = opt.WS_TMB_FULL
                                ? width
                                : width - (opt.DASH_VERTICAL ? 0 : dashWidthReservation);

            wsTmbHeight = this._workspacesThumbnails.get_preferred_height(wsTmbWidth)[0];
            wsTmbHeight = Math.round(Math.min(
                wsTmbHeight * expandFraction,
                height * opt.MAX_THUMBNAIL_SCALE
            ));

            wsTmbWidth = Math.round(Math.min(this._workspacesThumbnails.get_preferred_custom_width(wsTmbHeight)[1], wsTmbWidth));

            let wsTmbY;
            if (opt.WS_TMB_TOP) {
                wsTmbY = Math.round(startY + /*searchHeight + */(opt.DASH_TOP ? dashHeight : spacing / 2));
            } else {
                //const boxY = workArea.y - monitor.y; // startY might be compensated
                //wsTmbY = Math.round(boxY + height - (DASH_BOTTOM ? dashHeight : 0) - wsTmbHeight);
                wsTmbY = Math.round(startY + height - (opt.DASH_BOTTOM ? dashHeight : 0) - wsTmbHeight);
            }

            let wstOffset = (width - wsTmbWidth) / 2;
            wstOffset = wstOffset - opt.WS_TMB_POSITION_ADJUSTMENT * wstOffset;
            let wsTmbX = Math.round(Math.clamp(
                startX + wstOffset,
                startX + (opt.DASH_LEFT ? dashWidthReservation + spacing / 2 : spacing / 2),
                width - wsTmbWidth - startX - (opt.DASH_RIGHT ? dashWidthReservation + spacing / 2 : spacing / 2)
            ));

            childBox.set_origin(wsTmbX, wsTmbY);
            childBox.set_size(wsTmbWidth, wsTmbHeight);

            this._workspacesThumbnails.allocate(childBox);

            availableHeight -= wsTmbHeight + spacing;
        }


        if (this._dash.visible) {
            //const wMaxHeight = height - spacing - wsTmbHeight - 2 * spacing - (DASH_VERTICAL ? 0 : dashHeight + spacing);
            if (opt.WS_TMB_FULL && opt.DASH_VERTICAL) {
                const wMaxHeight = height - spacing - wsTmbHeight;
                this._dash.setMaxSize(maxDashWidth, wMaxHeight);
                [, dashWidth] = this._dash.get_preferred_width(wMaxHeight);
                [, dashHeight] = this._dash.get_preferred_height(dashWidth);
                dashWidth = Math.round(Math.min(dashWidth, maxDashWidth));
                dashHeight = Math.round(Math.min(dashHeight, wMaxHeight));
            }

            let dashX, dashY, offset;
            if (opt.DASH_RIGHT) {
                dashX = width - dashWidth;
            } else if (opt.DASH_LEFT) {
                dashX = 0;
            } else if (opt.DASH_TOP) {
                dashY = startY;
            } else {
                dashY = startY + height - dashHeight;
            }

            if (opt.DASH_VERTICAL) {
                if (opt.WS_TMB_FULL) {
                    offset = (height - dashHeight - wsTmbHeight) / 2;
                    if (opt.WS_TMB_TOP) {
                        offset = offset - opt.DASH_POSITION_ADJUSTMENT * (offset - spacing / 2);
                        dashY = startY + offset + wsTmbHeight;
                        //dashY = Math.max(dashY, startY + wsTmbHeight);
                    } else {
                        offset = offset - opt.DASH_POSITION_ADJUSTMENT * (offset - spacing / 2);
                        dashY = startY + offset;
                        //dashY = Math.max(dashY, height - wsTmbHeight - dashHeight - 3 * spacing);
                    }
                } else {
                    offset = (height - dashHeight) / 2;
                    offset = offset - opt.DASH_POSITION_ADJUSTMENT * (offset - spacing / 2);
                    dashY = startY + offset;
                }
            } else {
                offset = (width - dashWidth) / 2;
                dashX = startX + (offset - opt.DASH_POSITION_ADJUSTMENT * (offset - spacing));
            }

            childBox.set_origin(Math.round(startX + dashX), Math.round(dashY));
            childBox.set_size(dashWidth, dashHeight);
            this._dash.allocate(childBox);
        }

        availableHeight -= (opt.DASH_VERTICAL ? 0 : dashHeight);

        /*let [searchHeight] = this._searchEntry.get_preferred_height(width);*/

        // Workspaces
        let params = [box, workAreaBox, dashWidth, dashHeight, wsTmbHeight, searchHeight, startY];
        const transitionParams = this._stateAdjustment.getStateTransitionParams();

        // Update cached boxes
        for (const state of Object.values(ControlsState)) {
            this._cachedWorkspaceBoxes.set(
                state, this._computeWorkspacesBoxForState(state, ...params));
        }

        let workspacesBox;
        if (!transitionParams.transitioning) {
            workspacesBox = this._cachedWorkspaceBoxes.get(transitionParams.currentState);
        }
        if (!workspacesBox) {
            const initialBox = this._cachedWorkspaceBoxes.get(transitionParams.initialState);
            const finalBox = this._cachedWorkspaceBoxes.get(transitionParams.finalState);
            workspacesBox = initialBox.interpolate(finalBox, transitionParams.progress);
        }

        this._workspacesDisplay.allocate(workspacesBox);

        // Search entry
        const searchXoffset = (opt.DASH_LEFT ? dashWidth : 0) + spacing;
        //let [searchHeight] = this._searchEntry.get_preferred_height(width - wsTmbWidth);

        // Y position under top Dash
        let searchEntryX, searchEntryY;
        /*if (opt.OVERVIEW_MODE2 && !opt.DASH_TOP && !opt.WS_TMB_TOP) {
            searchEntryY = 7;
        } else */if (opt.DASH_TOP) {
            searchEntryY = startY + (opt.WS_TMB_TOP ? wsTmbHeight : 0) + dashHeight - spacing;
        } else {
            searchEntryY = startY + (opt.WS_TMB_TOP ? wsTmbHeight + spacing : 0);
        }

        searchEntryX = searchXoffset;
        let searchWidth = width - 2 * spacing - (opt.DASH_VERTICAL ? dashWidth : 0); // xAlignCenter is given by wsBox
        searchWidth = this._xAlignCenter ? width : searchWidth;

        if (opt.CENTER_SEARCH_VIEW) {
            childBox.set_origin(0, searchEntryY);
            childBox.set_size(width, searchHeight);
        } else {
            childBox.set_origin(this._xAlignCenter ? 0 : searchEntryX, searchEntryY);
            childBox.set_size(this._xAlignCenter ? width : searchWidth - spacing, searchHeight);
        }

        this._searchEntry.allocate(childBox);

        availableHeight -= searchHeight + spacing;

        // AppDisplay - state, box, workAreaBox, searchHeight, dashHeight, appGridBox, wsTmbWidth
        //if (this._appDisplay.visible) {
            params = [box, workAreaBox, searchHeight, dashWidth, dashHeight, wsTmbHeight, startY];
            let appDisplayBox;
            if (!transitionParams.transitioning) {
                appDisplayBox =
                    this._getAppDisplayBoxForState(transitionParams.currentState, ...params);
            } else {
                const initialBox =
                    this._getAppDisplayBoxForState(transitionParams.initialState, ...params);
                const finalBox =
                    this._getAppDisplayBoxForState(transitionParams.finalState, ...params);

                appDisplayBox = initialBox.interpolate(finalBox, transitionParams.progress);
            }
            this._appDisplay.allocate(appDisplayBox);
        //}

        // Search
        if (opt.CENTER_SEARCH_VIEW) {
            const dashW = (opt.DASH_VERTICAL ? dashWidth : 0) + spacing;
            searchWidth = width - 2 * dashW;
            childBox.set_origin(dashW, startY + (opt.DASH_TOP ? dashHeight : spacing) + (opt.WS_TMB_TOP ? wsTmbHeight + spacing : 0) + searchHeight);
        } else {
            childBox.set_origin(this._xAlignCenter ? spacing : searchXoffset, startY + (opt.DASH_TOP ? dashHeight : spacing) + (opt.WS_TMB_TOP ? wsTmbHeight + spacing : 0) + searchHeight);
        }

        childBox.set_size(searchWidth, availableHeight);
        this._searchController.allocate(childBox);

        this._runPostAllocation();
    }
}

// same copy of this function should be available in OverviewControls and WorkspacesView
function _getFitModeForState(state) {
    switch (state) {
    case ControlsState.HIDDEN:
    case ControlsState.WINDOW_PICKER:
        return FitMode.SINGLE;
    case ControlsState.APP_GRID:
        if (opt.WS_ANIMATION && opt.SHOW_WS_TMB)
            return FitMode.ALL;
        else
            return FitMode.SINGLE;
    default:
        return FitMode.SINGLE;
    }
}
