diff --git a/src/config/bismuth_config.kcfg b/src/config/bismuth_config.kcfg
index f4e57ede..a0102cfb 100644
--- a/src/config/bismuth_config.kcfg
+++ b/src/config/bismuth_config.kcfg
@@ -48,6 +48,11 @@
false
+
+
+ false
+
+
diff --git a/src/core/ts-proxy.cpp b/src/core/ts-proxy.cpp
index 0fa25a9f..32314a73 100644
--- a/src/core/ts-proxy.cpp
+++ b/src/core/ts-proxy.cpp
@@ -54,6 +54,7 @@ QJSValue TSProxy::jsConfig()
addLayout("enableQuarterLayout", "QuarterLayout");
addLayout("enableFloatingLayout", "FloatingLayout");
addLayout("enableCascadeLayout", "CascadeLayout");
+ addLayout("enableVerticalTileLayout", "VerticalTileLayout");
setProp("monocleMaximize", m_config.monocleMaximize());
setProp("maximizeSoleTile", m_config.maximizeSoleTile());
diff --git a/src/kcm/package/contents/ui/views/Layouts.qml b/src/kcm/package/contents/ui/views/Layouts.qml
index cd59dc1d..e62cccb0 100644
--- a/src/kcm/package/contents/ui/views/Layouts.qml
+++ b/src/kcm/package/contents/ui/views/Layouts.qml
@@ -57,6 +57,11 @@ Kirigami.Page {
settingName: "enableFloatingLayout"
}
+ ListElement {
+ name: "Vertical Tile"
+ settingName: "enableVerticalTileLayout"
+ }
+
}
KCM.ScrollView {
diff --git a/src/kwinscript/controller/action.ts b/src/kwinscript/controller/action.ts
index c83da63f..9599fdb4 100644
--- a/src/kwinscript/controller/action.ts
+++ b/src/kwinscript/controller/action.ts
@@ -570,6 +570,19 @@ export class ToggleSpiralLayout extends ToggleCurrentLayout {
}
}
+export class ToggleVerticalTileLayout extends ToggleCurrentLayout {
+ constructor(protected engine: Engine, protected log: Log) {
+ super(
+ engine,
+ "VerticalTileLayout",
+ "toggle_vertical_tile_layout",
+ "Toggle Vertical Tile Layout",
+ "",
+ log
+ );
+ }
+}
+
export class Rotate extends ActionImpl implements Action {
constructor(protected engine: Engine, protected log: Log) {
super(engine, "rotate", "Rotate", "Meta+R", log);
diff --git a/src/kwinscript/controller/index.ts b/src/kwinscript/controller/index.ts
index aae1af0d..a3c88695 100644
--- a/src/kwinscript/controller/index.ts
+++ b/src/kwinscript/controller/index.ts
@@ -424,6 +424,7 @@ export class ControllerImpl implements Controller {
new Action.ToggleFloatingLayout(this.engine, this.log),
new Action.ToggleQuarterLayout(this.engine, this.log),
new Action.ToggleSpiralLayout(this.engine, this.log),
+ new Action.ToggleVerticalTileLayout(this.engine, this.log),
new Action.Rotate(this.engine, this.log),
new Action.RotateReverse(this.engine, this.log),
diff --git a/src/kwinscript/engine/layout/vertical_layout.ts b/src/kwinscript/engine/layout/vertical_layout.ts
new file mode 100644
index 00000000..c95115eb
--- /dev/null
+++ b/src/kwinscript/engine/layout/vertical_layout.ts
@@ -0,0 +1,223 @@
+// SPDX-FileCopyrightText: 2022 Diogo Ferreira
+//
+// SPDX-License-Identifier: MIT
+
+import { WindowsLayout } from ".";
+import LayoutUtils from "./layout_utils";
+
+import { WindowState, EngineWindow } from "../window";
+
+import {
+ Action,
+ DecreaseLayoutMasterAreaSize,
+ IncreaseLayoutMasterAreaSize,
+ IncreaseMasterAreaWindowCount,
+ DecreaseMasterAreaWindowCount,
+ Rotate,
+ RotateReverse,
+} from "../../controller/action";
+
+import { partitionArrayBySizes, clip, slide } from "../../util/func";
+import { Rect, RectDelta } from "../../util/rect";
+import { Config } from "../../config";
+import { Controller } from "../../controller";
+import { Engine } from "..";
+
+/**
+ * A Vertical tiling layout which is tailored for portrait orientated
+ * monitors but might have use cases in landscape as well.
+ *
+ * The intended behavior is for the master area to take a fixed (but
+ * configurable via shortcut) percentage of the screen and for the
+ * remaining panes to split the remaining space. Multiple masters are
+ * supported and will share the master area equally.
+ *
+ * ---------
+ * | |
+ * | M1 |
+ * | |
+ * | ------- |
+ * | |
+ * | M2 |
+ * | |
+ * ---------
+ * | 1 |
+ * ---------
+ * | 2 |
+ * ---------
+ */
+export default class VerticalTileLayout implements WindowsLayout {
+ public static readonly MIN_MASTER_RATIO = 0.2;
+ public static readonly MAX_MASTER_RATIO = 0.75;
+ public static readonly id = "VerticalTileLayout";
+ public readonly classID = VerticalTileLayout.id;
+ public readonly name = "Vertical Tile Layout";
+ public readonly icon = "bismuth-vertical-tile";
+
+ public get hint(): string {
+ return String(this.masterCount);
+ }
+
+ private masterRatio: number;
+ private masterCount: number;
+
+ private config: Config;
+
+ constructor(config: Config) {
+ this.config = config;
+ this.masterRatio = 0.75;
+ this.masterCount = 1;
+ }
+
+ public adjust(
+ area: Rect,
+ tiles: EngineWindow[],
+ basis: EngineWindow,
+ delta: RectDelta
+ ): void {
+ const basisIndex = tiles.indexOf(basis);
+ if (basisIndex < 0) {
+ return;
+ }
+
+ if (tiles.length === 0) {
+ /* no tiles */
+ return;
+ } else if (tiles.length <= this.masterCount) {
+ /* every window takes a piece of the master area */
+ LayoutUtils.adjustAreaWeights(
+ area,
+ tiles.map((tile) => tile.weight),
+ this.config.tileLayoutGap,
+ tiles.indexOf(basis),
+ delta
+ ).forEach((newWeight, i) => (tiles[i].weight = newWeight * tiles.length));
+ } else if (tiles.length > this.masterCount) {
+ /* Two rows */
+ let basisGroup;
+ if (basisIndex < this.masterCount) {
+ /* master area */
+ basisGroup = 1;
+ } else {
+ /* bottom stack */
+ basisGroup = 0;
+ }
+
+ /* adjust master-stack ratio */
+ const stackRatio = 1 - this.masterRatio;
+ const newRatios = LayoutUtils.adjustAreaWeights(
+ area,
+ [stackRatio, this.masterRatio, stackRatio],
+ this.config.tileLayoutGap,
+ basisGroup,
+ delta,
+ false /* vertical */
+ );
+ const newMasterRatio = newRatios[1];
+ const newStackRatio = basisGroup === 0 ? newRatios[0] : newRatios[2];
+ this.masterRatio = newMasterRatio / (newMasterRatio + newStackRatio);
+
+ /* adjust tile weight */
+ const bottomStackNumTiles = tiles.length - this.masterCount;
+ const [masterTiles, bottomStackTiles] =
+ partitionArrayBySizes(tiles, [
+ this.masterCount,
+ bottomStackNumTiles,
+ ]);
+ const groupTiles = [masterTiles, bottomStackTiles][basisGroup];
+ LayoutUtils.adjustAreaWeights(
+ area /* we only need height */,
+ groupTiles.map((tile) => tile.weight),
+ this.config.tileLayoutGap,
+ groupTiles.indexOf(basis),
+ delta,
+ false /* vertical */
+ ).forEach(
+ (newWeight, i) => (groupTiles[i].weight = newWeight * groupTiles.length)
+ );
+ }
+ }
+
+ public apply(
+ _controller: Controller,
+ tileables: EngineWindow[],
+ area: Rect
+ ): void {
+ /* Tile all tileables */
+ tileables.forEach((tileable) => (tileable.state = WindowState.Tiled));
+ const tiles = tileables;
+
+ if (tiles.length <= this.masterCount) {
+ /* only master */
+ LayoutUtils.splitAreaWeighted(
+ area,
+ tiles.map((tile) => tile.weight),
+ this.config.tileLayoutGap,
+ false /* vertical */
+ ).forEach((tileArea, i) => (tiles[i].geometry = tileArea));
+ } else {
+ /* master & bottom-stack */
+ const stackRatio = 1 - this.masterRatio;
+
+ /** Areas allocated to master, and bottom-stack */
+ const groupAreas = LayoutUtils.splitAreaWeighted(
+ area,
+ [this.masterRatio, stackRatio],
+ this.config.tileLayoutGap,
+ false /* vertical */
+ );
+
+ const bottomStackNumTiles = tiles.length - this.masterCount;
+ const [masterTiles, bottomStackTiles] =
+ partitionArrayBySizes(tiles, [
+ this.masterCount,
+ bottomStackNumTiles,
+ ]);
+ [masterTiles, bottomStackTiles].forEach((groupTiles, group) => {
+ LayoutUtils.splitAreaWeighted(
+ groupAreas[group],
+ groupTiles.map((tile) => tile.weight),
+ this.config.tileLayoutGap,
+ false /* vertical */
+ ).forEach((tileArea, i) => (groupTiles[i].geometry = tileArea));
+ });
+ }
+ }
+
+ public clone(): WindowsLayout {
+ const other = new VerticalTileLayout(this.config);
+ other.masterRatio = this.masterRatio;
+ return other;
+ }
+
+ public executeAction(engine: Engine, action: Action): void {
+ if (action instanceof DecreaseLayoutMasterAreaSize) {
+ this.masterRatio = clip(
+ slide(this.masterRatio, -0.05),
+ VerticalTileLayout.MIN_MASTER_RATIO,
+ VerticalTileLayout.MAX_MASTER_RATIO
+ );
+ } else if (action instanceof IncreaseLayoutMasterAreaSize) {
+ this.masterRatio = clip(
+ slide(this.masterRatio, +0.05),
+ VerticalTileLayout.MIN_MASTER_RATIO,
+ VerticalTileLayout.MAX_MASTER_RATIO
+ );
+ } else if (action instanceof IncreaseMasterAreaWindowCount) {
+ this.resizeMaster(engine, 1);
+ } else if (action instanceof DecreaseMasterAreaWindowCount) {
+ this.resizeMaster(engine, -1);
+ } else {
+ action.executeWithoutLayoutOverride();
+ }
+ }
+
+ public toString(): string {
+ return `VerticalTileLayout(masterCount=${this.masterCount})`;
+ }
+
+ private resizeMaster(engine: Engine, step: -1 | 1): void {
+ this.masterCount = clip(this.masterCount + step, 1, 10);
+ engine.showLayoutNotification();
+ }
+}
diff --git a/src/kwinscript/engine/layout_store.ts b/src/kwinscript/engine/layout_store.ts
index 8d1e8a68..86a773e7 100644
--- a/src/kwinscript/engine/layout_store.ts
+++ b/src/kwinscript/engine/layout_store.ts
@@ -19,6 +19,7 @@ import SpiralLayout from "./layout/spiral_layout";
import SpreadLayout from "./layout/spread_layout";
import StairLayout from "./layout/stair_layout";
import ThreeColumnLayout from "./layout/three_column_layout";
+import VerticalTileLayout from "./layout/vertical_layout";
export class LayoutStoreEntry {
public get currentLayout(): WindowsLayout {
@@ -96,6 +97,8 @@ export class LayoutStoreEntry {
return new ThreeColumnLayout(this.config);
} else if (id == TileLayout.id) {
return new TileLayout(this.config);
+ } else if (id == VerticalTileLayout.id) {
+ return new VerticalTileLayout(this.config);
} else {
return new FloatingLayout();
}
diff --git a/src/kwinscript/icons/16-status-bismuth-vertical-tile.svg b/src/kwinscript/icons/16-status-bismuth-vertical-tile.svg
new file mode 100644
index 00000000..e11b1df6
--- /dev/null
+++ b/src/kwinscript/icons/16-status-bismuth-vertical-tile.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/src/kwinscript/icons/32-status-bismuth-vertical-tile.svg b/src/kwinscript/icons/32-status-bismuth-vertical-tile.svg
new file mode 100644
index 00000000..bd9e808f
--- /dev/null
+++ b/src/kwinscript/icons/32-status-bismuth-vertical-tile.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/src/kwinscript/icons/CMakeLists.txt b/src/kwinscript/icons/CMakeLists.txt
index 0c73ca01..a0e99571 100644
--- a/src/kwinscript/icons/CMakeLists.txt
+++ b/src/kwinscript/icons/CMakeLists.txt
@@ -11,6 +11,7 @@ ecm_install_icons(
16-status-bismuth-spread.svg
16-status-bismuth-stair.svg
16-status-bismuth-tile.svg
+ 16-status-bismuth-vertical-tile.svg
32-status-bismuth-column.svg
32-status-bismuth-floating.svg
32-status-bismuth-monocle.svg
@@ -19,5 +20,6 @@ ecm_install_icons(
32-status-bismuth-spread.svg
32-status-bismuth-stair.svg
32-status-bismuth-tile.svg
+ 32-status-bismuth-vertical-tile.svg
DESTINATION
${KDE_INSTALL_ICONDIR})