Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ Open Cinnamon Extensions, click on the Fancy Tiles extension and click the '+' b

## Quick start

After enabling the extension, press `<SUPER>+G` to open the layout editor. It will start by a 2x2 grid layout. Click and drag the dividers (the lines between regions) to resize the regions. If you want to split a region, press `<SHIFT>` or `<CTRL>` while hovering over the region to split the region horizontally or vertically. Use the `right mouse button` to remove dividers. Use `<Page Up>` and `<Page Down>` to increase or decrease the spacing between the regions.
After enabling the extension, press `<SUPER>+G` to open the layout editor. It will start with a 2x2 grid layout. Click and drag the dividers (the lines between regions) to resize the regions. If you want to split a region, press `<SHIFT>` or `<CTRL>` while hovering over the region to split the region horizontally or vertically. Use the `right mouse button` to remove dividers. Use `<Page Up>` and `<Page Down>` to increase or decrease the spacing between the regions.

After you have crafted your desired layout, exit the editor using `<SUPER>+G` or `<ESC>`.

![Layout editor](docs/layout-editor.png)

Now, start dragging a window and simultaneously hold the `<CTRL>` key. The layout will become visible. Hover your mose over the region you want the window to snap to and release the mouse button. The window will now be snapped into place.
Now, start dragging a window and simultaneously hold the `<CTRL>` key. The layout will become visible. Hover your mouse over the region you want the window to snap to and release the mouse button. The window will now be snapped into place. When the mouse hovers over the border between two regions, these regions are merged into a single region into which the window will snap.

If you want to make the snapping region even larger you can hold the `<ALT>` key and hover over adjacent regions to merge them all into a single large snapping region.

![Layout editor](docs/window-snapping.png)

Expand Down
19 changes: 11 additions & 8 deletions application.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ function mapModifierSettingToModifierType(modifierSetting) {
return [Clutter.ModifierType.SUPER_MASK, Clutter.ModifierType.MOD4_MASK];
case 'SHIFT':
return [Clutter.ModifierType.SHIFT_MASK];
default:
default:
return [];
}
}

// The application class is only constructed once and is the main entry
// The application class is only constructed once and is the main entry
// of the extension.
class Application {
// the active grid editor
Expand Down Expand Up @@ -132,7 +132,7 @@ class Application {
}

#loadThemeColors() {
// hidden element to fetch the styling
// hidden element to fetch the styling
let stylingActor = new St.DrawingArea({
style_class: 'tile-preview tile-hud',
visible: false
Expand Down Expand Up @@ -182,7 +182,7 @@ class Application {
}

#enableHotkey() {
this.#disableHotkey();
this.#disableHotkey();
Main.keybindingManager.addHotKey('fancytiles', this.#settings.settingsData.hotkey.value, this.#toggleEditor.bind(this));
}

Expand Down Expand Up @@ -269,16 +269,19 @@ class Application {
#connectWindowGrabs() {
// start snapping when the user starts moving a window
this.#signals.connect(global.display, 'grab-op-begin', (display, screen, window, op) => {
if (op === Meta.GrabOp.MOVING && window.window_type === Meta.WindowType.NORMAL) {
if (op === Meta.GrabOp.MOVING && window.window_type === Meta.WindowType.NORMAL) {
// reload styling
this.#loadThemeColors();
const enableSnappingModifiers = mapModifierSettingToModifierType(this.#settings.settingsData.enableSnappingModifiers.value);

const enableMultiSnappingModifiers = mapModifierSettingToModifierType(this.#settings.settingsData.enableMultiSnappingModifiers.value);
const enableMergeAdjacentOnHover = this.#settings.settingsData.mergeAdjacentOnHover.value;
const mergingRadius = this.#settings.settingsData.mergingRadius.value;

// Create WindowSnapper for each monitor
const nMonitors = global.display.get_n_monitors();
for (let i = 0; i < nMonitors; i++) {
const layout = this.#readOrCreateLayoutForDisplay(i, LayoutOf2x2);
const snapper = new WindowSnapper(i, layout, window, enableSnappingModifiers);
const snapper = new WindowSnapper(i, layout, window, enableSnappingModifiers, enableMultiSnappingModifiers, enableMergeAdjacentOnHover, mergingRadius);
this.#windowSnappers.push(snapper);
}
}
Expand All @@ -300,4 +303,4 @@ class Application {
}
}

module.exports = { Application, LayoutOf2x2 };
module.exports = { Application, LayoutOf2x2 };
125 changes: 93 additions & 32 deletions drawing.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// this module contains functionality to draw the layout of a node tree
// on a given Cairo context

const TAU = Math.PI * 2;

// blueish default / fallback colors
const DefaultColors = {
background: {
Expand All @@ -25,41 +23,89 @@ const DefaultColors = {
}
}

function drawRoundedRect(cr, rect, radius, fillColor, strokeColor) {
let { x, y, width, height } = rect;

let drawPath = function () {
// Start a new path for the rounded rectangle
cr.newPath();

// Move to starting point
cr.moveTo(x + radius, y);
const { buildDifferencePath, polygonArea } = require('./shapes');

// Top edge and top-right corner
cr.lineTo(x + width - radius, y);
cr.arc(x + width - radius, y + radius, radius, -TAU / 4, 0);
function drawRoundedRect(cr, rect, radius, fillColor, strokeColor, excludedRect) {
const pathPoints = buildDifferencePath(rect, excludedRect);
if (!pathPoints || pathPoints.length < 3) {
return;
}

// Right edge and bottom-right corner
cr.lineTo(x + width, y + height - radius);
cr.arc(x + width - radius, y + height - radius, radius, 0, TAU / 4);
const drawPath = function () {
cr.newPath();

// Bottom edge and bottom-left corner
cr.lineTo(x + radius, y + height);
cr.arc(x + radius, y + height - radius, radius, TAU / 4, TAU / 2);
let polygon = pathPoints.slice();
if (polygonArea(polygon) < 0) {
polygon = polygon.reverse();
}

// Left edge and top-left corner
cr.lineTo(x, y + radius);
cr.arc(x + radius, y + radius, radius, TAU / 2, TAU * 3 / 4);
const pointCount = polygon.length;
const cornerRadiusInput = Math.max(0, radius);

for (let i = 0; i < pointCount; i++) {
const prev = polygon[(i - 1 + pointCount) % pointCount];
const current = polygon[i];
const next = polygon[(i + 1) % pointCount];

let dirIn = { x: current.x - prev.x, y: current.y - prev.y };
let dirOut = { x: next.x - current.x, y: next.y - current.y };

const lenIn = Math.hypot(dirIn.x, dirIn.y);
const lenOut = Math.hypot(dirOut.x, dirOut.y);

if (lenIn === 0 || lenOut === 0) {
continue;
}

dirIn = { x: dirIn.x / lenIn, y: dirIn.y / lenIn };
dirOut = { x: dirOut.x / lenOut, y: dirOut.y / lenOut };

const cornerRadius = Math.min(cornerRadiusInput, lenIn / 2, lenOut / 2);
// trim the straight segments so the arc touches correct tangents
const startPoint = {
x: current.x - dirIn.x * cornerRadius,
y: current.y - dirIn.y * cornerRadius
};
const endPoint = {
x: current.x + dirOut.x * cornerRadius,
y: current.y + dirOut.y * cornerRadius
};

if (i === 0) {
cr.moveTo(startPoint.x, startPoint.y);
} else {
cr.lineTo(startPoint.x, startPoint.y);
}

const turn = dirIn.x * dirOut.y - dirIn.y * dirOut.x;

if (cornerRadius > 1e-6 && Math.abs(turn) > 1e-6) {
// center sits in the quadrant spanned by dirIn/dirOut; sign of turn decides arc direction
const center = {
x: current.x - dirIn.x * cornerRadius + dirOut.x * cornerRadius,
y: current.y - dirIn.y * cornerRadius + dirOut.y * cornerRadius
};
const startAngle = Math.atan2(startPoint.y - center.y, startPoint.x - center.x);
const endAngle = Math.atan2(endPoint.y - center.y, endPoint.x - center.x);

if (turn > 0) {
cr.arc(center.x, center.y, cornerRadius, startAngle, endAngle);
} else {
cr.arcNegative(center.x, center.y, cornerRadius, startAngle, endAngle);
}
} else {
// no curved corner here; continue with straight run
cr.lineTo(endPoint.x, endPoint.y);
}
}

cr.closePath();
}
};

// fill the region
cr.setSourceRGBA(fillColor.r, fillColor.g, fillColor.b, fillColor.a);
drawPath();
cr.fill();

// draw the border
cr.setSourceRGBA(strokeColor.r, strokeColor.g, strokeColor.b, strokeColor.a);
drawPath();
cr.stroke();
Expand All @@ -74,18 +120,28 @@ function addMargins(rect, margin) {
}
}

function drawLayout(cr, node, displayRect, colors = DefaultColors, cornerRadius = 10) {
function drawLayout(cr, node, displayRect, colors = DefaultColors, cornerRadius = 10, cutoutRect = null) {
if (!node) return;

// Draw current node
let rect = node.rect;
let rect = node.rect;

// Offset by monitor displayRect
let x = rect.x - displayRect.x;
let y = rect.y - displayRect.y;
let width = rect.width;
let height = rect.height;

// do we have a cutout for all children?
if(node.insetNode && !cutoutRect){
cutoutRect = addMargins({
x: node.insetNode.rect.x - displayRect.x,
y: node.insetNode.rect.y - displayRect.y,
width: node.insetNode.rect.width,
height: node.insetNode.rect.height
}, -node.margin*2);
}

// draw the region of a leaf node
if (node.isLeaf()) {
let c = colors.background;
Expand All @@ -95,16 +151,21 @@ function drawLayout(cr, node, displayRect, colors = DefaultColors, cornerRadius
}
cr.setSourceRGBA(c.r, c.g, c.b, c.a);

let regionRect = addMargins({ x, y, width, height }, node.margin);
drawRoundedRect(cr, regionRect, cornerRadius, c, colors.border);
let regionRect = addMargins({ x, y, width, height }, node.margin);
drawRoundedRect(cr, regionRect, cornerRadius, c, colors.border, cutoutRect);
}

for (let child of node.children) {
drawLayout(cr, child, displayRect, colors, cornerRadius);
drawLayout(cr, child, displayRect, colors, cornerRadius, cutoutRect);
}

// draw the cutout once here
if (node.insetNode) {
drawLayout(cr, node.insetNode, displayRect, colors, cornerRadius, null);
}
}

module.exports = {
drawLayout,
DefaultColors
};
};
Loading