From ddce3bb9e0c29a150bcd524eead03ff817b022f8 Mon Sep 17 00:00:00 2001 From: lutechi Date: Wed, 1 Oct 2025 21:30:59 -0400 Subject: [PATCH 1/3] Add zone numbering with live dimensions and text shadows --- README.md | 8 +++++-- drawing.js | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 76d833b..aa741e6 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,17 @@ Open Cinnamon Extensions, click on the Fancy Tiles extension and click the '+' b ## Quick start -After enabling the extension, press `+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 `` or `` while hovering over the region to split the region horizontally or vertically. Use the `right mouse button` to remove dividers. Use `` and `` to increase or decrease the spacing between the regions. +After enabling the extension, press `+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 `` or `` while hovering over the region to split the region horizontally or vertically. Use the `right mouse button` to remove dividers. Use `` and `` to increase or decrease the spacing between the regions. After you have crafted your desired layout, exit the editor using `+G` or ``. ![Layout editor](docs/layout-editor.png) -Now, start dragging a window and simultaneously hold the `` 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 `` 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. + +### Multi-zone spanning + +Windows can now span across multiple adjacent regions. When dragging a window near the edge between two adjacent regions, both regions will be highlighted, and the window will snap to cover the combined area of both regions. ![Layout editor](docs/window-snapping.png) diff --git a/drawing.js b/drawing.js index 18cc04e..7dd2692 100644 --- a/drawing.js +++ b/drawing.js @@ -74,7 +74,7 @@ function addMargins(rect, margin) { } } -function drawLayout(cr, node, displayRect, colors = DefaultColors, cornerRadius = 10) { +function drawLayout(cr, node, displayRect, colors = DefaultColors, cornerRadius = 10, zoneCounter = { count: 0 }) { if (!node) return; // Draw current node @@ -97,10 +97,71 @@ function drawLayout(cr, node, displayRect, colors = DefaultColors, cornerRadius let regionRect = addMargins({ x, y, width, height }, node.margin); drawRoundedRect(cr, regionRect, cornerRadius, c, colors.border); + + // Increment and draw zone number with size + zoneCounter.count++; + const zoneNumber = zoneCounter.count; + + // Draw zone number and size (width x height in pixels) - use regionRect for live updates + const sizeText = `${Math.round(regionRect.width)}x${Math.round(regionRect.height)}`; + + // Standard uniform font sizes - scale down only if zone is too small + const baseNumberSize = 72; // doubled for 4K visibility + const baseSizeTextSize = 36; // doubled for 4K visibility + const minDimension = Math.min(regionRect.width, regionRect.height); + const scaleFactor = Math.min(1, minDimension / 200); // scale down if smaller than 200px + + const numberFontSize = baseNumberSize * scaleFactor; + const sizeFontSize = baseSizeTextSize * scaleFactor; + + cr.setSourceRGBA(colors.border.r, colors.border.g, colors.border.b, 1); + cr.selectFontFace('Sans', 0, 1); // Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD + + // Measure text to center it + cr.setFontSize(numberFontSize); + const numberExtents = cr.textExtents(zoneNumber.toString()); + + cr.setFontSize(sizeFontSize); + const sizeExtents = cr.textExtents(sizeText); + + // Calculate center position + const centerX = regionRect.x + regionRect.width / 2; + const centerY = regionRect.y + regionRect.height / 2; + const totalHeight = numberExtents.height + sizeExtents.height + 15; // 15px spacing (increased) + + // Draw zone number with shadow for readability (centered, larger) + cr.setFontSize(numberFontSize); + const numberX = centerX - numberExtents.width / 2; + const numberY = centerY - totalHeight / 2 + numberExtents.height; + + // Shadow/outline for zone number + cr.setSourceRGBA(0, 0, 0, 0.8); // dark shadow + cr.moveTo(numberX + 2, numberY + 2); + cr.showText(zoneNumber.toString()); + + // Main zone number text + cr.setSourceRGBA(colors.border.r, colors.border.g, colors.border.b, 1); + cr.moveTo(numberX, numberY); + cr.showText(zoneNumber.toString()); + + // Draw size with shadow for readability (centered, smaller, below number) + cr.setFontSize(sizeFontSize); + const sizeX = centerX - sizeExtents.width / 2; + const sizeY = centerY + totalHeight / 2; + + // Shadow/outline for size text + cr.setSourceRGBA(0, 0, 0, 0.8); // dark shadow + cr.moveTo(sizeX + 2, sizeY + 2); + cr.showText(sizeText); + + // Main size text + cr.setSourceRGBA(colors.border.r, colors.border.g, colors.border.b, 1); + cr.moveTo(sizeX, sizeY); + cr.showText(sizeText); } for (let child of node.children) { - drawLayout(cr, child, displayRect, colors, cornerRadius); + drawLayout(cr, child, displayRect, colors, cornerRadius, zoneCounter); } } From a01ff6663850d8b096f15fb6fd4ccb07cb888b7c Mon Sep 17 00:00:00 2001 From: lutechi Date: Wed, 1 Oct 2025 21:31:57 -0400 Subject: [PATCH 2/3] Add multi-zone window spanning feature --- node_tree.js | 135 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 124 insertions(+), 11 deletions(-) diff --git a/node_tree.js b/node_tree.js index d4409f0..be202d3 100644 --- a/node_tree.js +++ b/node_tree.js @@ -327,6 +327,63 @@ class LayoutNode { return this.children.reduce((found, child) => found || child.findNodeAtPosition(x, y), null); } + // find all leaf nodes near the given position (for multi-zone spanning) + findNodesNearPosition(x, y, threshold = 60) { + let nearbyNodes = []; + let insideNodes = []; + + this.forSelfAndDescendants(node => { + if (!node.isLeaf()) return; + + // Check if inside the node (highest priority) + let insideX = x >= node.rect.x && x <= node.rect.x + node.rect.width; + let insideY = y >= node.rect.y && y <= node.rect.y + node.rect.height; + + if (insideX && insideY) { + insideNodes.push(node); + return; + } + + // Check if position is within threshold of any edge + let nearLeft = Math.abs(x - node.rect.x) <= threshold; + let nearRight = Math.abs(x - (node.rect.x + node.rect.width)) <= threshold; + let nearTop = Math.abs(y - node.rect.y) <= threshold; + let nearBottom = Math.abs(y - (node.rect.y + node.rect.height)) <= threshold; + + // Include node if position is near edges (but prioritize inside nodes) + if ((insideX || nearLeft || nearRight) && (insideY || nearTop || nearBottom)) { + nearbyNodes.push(node); + } + }); + + // Return inside nodes with highest priority, then nearby nodes + return insideNodes.length > 0 ? [...insideNodes, ...nearbyNodes] : nearbyNodes; + } + + // check if two nodes are adjacent (share an edge) + areNodesAdjacent(node1, node2) { + if (!node1 || !node2) return false; + + let tolerance = 15; + + // Check horizontal adjacency (left/right) + let horizontalOverlap = !(node1.rect.y + node1.rect.height <= node2.rect.y + tolerance || + node2.rect.y + node2.rect.height <= node1.rect.y + tolerance); + + let node1RightOfNode2 = Math.abs(node1.rect.x - (node2.rect.x + node2.rect.width)) <= tolerance; + let node2RightOfNode1 = Math.abs(node2.rect.x - (node1.rect.x + node1.rect.width)) <= tolerance; + + // Check vertical adjacency (top/bottom) + let verticalOverlap = !(node1.rect.x + node1.rect.width <= node2.rect.x + tolerance || + node2.rect.x + node2.rect.width <= node1.rect.x + tolerance); + + let node1BelowNode2 = Math.abs(node1.rect.y - (node2.rect.y + node2.rect.height)) <= tolerance; + let node2BelowNode1 = Math.abs(node2.rect.y - (node1.rect.y + node1.rect.height)) <= tolerance; + + return (horizontalOverlap && (node1RightOfNode2 || node2RightOfNode1)) || + (verticalOverlap && (node1BelowNode2 || node2BelowNode1)); + } + // get the rectangle of the divider for this node, useful for grabbing and moving the divider getDividerRect(dividerWidth) { dividerWidth = Math.max(dividerWidth, 2 * this.margin); @@ -751,31 +808,87 @@ class SnappingOperation extends LayoutOperation { return this.cancel(); } - // Find node at mouse position - let node = this.tree.findNodeAtPosition(x, y); - if (!node) { + // Find nodes near mouse position for multi-zone spanning + let nearbyNodes = this.tree.findNodesNearPosition(x, y); + if (!nearbyNodes || nearbyNodes.length === 0) { return this.cancel(); } - // activate the region to snap into - this.showRegions = true; - + // Reset all snapping destinations and highlighting this.tree.forSelfAndDescendants(n => { n.isSnappingDestination = false; n.isHighlighted = false; }); - node.isSnappingDestination = true; - node.isHighlighted = true; + // For multi-zone spanning, check if nodes are adjacent + if (nearbyNodes.length > 1) { + let adjacentNodes = []; + let mainNode = nearbyNodes[0]; // The primary node + adjacentNodes.push(mainNode); + + // Find all nodes adjacent to the main node + for (let i = 1; i < nearbyNodes.length; i++) { + if (this.tree.areNodesAdjacent(mainNode, nearbyNodes[i])) { + adjacentNodes.push(nearbyNodes[i]); + } + } + + // Set all adjacent nodes as snapping destinations + adjacentNodes.forEach(node => { + node.isSnappingDestination = true; + node.isHighlighted = true; + }); + } else { + // Single zone + nearbyNodes[0].isSnappingDestination = true; + nearbyNodes[0].isHighlighted = true; + } + + this.showRegions = true; return OperationResult.handledAndRedraw(); } currentSnapToRect() { - var snapToNode = this.tree.findNode(n => n.isSnappingDestination); - if (!snapToNode) { + // Find all nodes that are snapping destinations + let snapToNodes = []; + this.tree.forSelfAndDescendants(n => { + if (n.isSnappingDestination) { + snapToNodes.push(n); + } + }); + + if (snapToNodes.length === 0) { return null; } - return snapToNode.snapRect(); + + if (snapToNodes.length === 1) { + // Single zone + return snapToNodes[0].snapRect(); + } + + // Multiple zones - calculate combined rectangle + let combinedRect = null; + snapToNodes.forEach(node => { + let nodeRect = node.snapRect(); + if (!combinedRect) { + combinedRect = { ...nodeRect }; + } else { + // Expand combined rectangle to include this node + let left = Math.min(combinedRect.x, nodeRect.x); + let top = Math.min(combinedRect.y, nodeRect.y); + let right = Math.max(combinedRect.x + combinedRect.width, nodeRect.x + nodeRect.width); + let bottom = Math.max(combinedRect.y + combinedRect.height, nodeRect.y + nodeRect.height); + + combinedRect = { + x: left, + y: top, + width: right - left, + height: bottom - top + }; + } + }); + + return combinedRect; } cancel() { From 81ff9bb5fb04158efa25b831ad2d4c0cf66fe9e0 Mon Sep 17 00:00:00 2001 From: lutechi Date: Fri, 3 Oct 2025 11:50:51 -0400 Subject: [PATCH 3/3] Add gap/margin display to zone overlays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shows the gap size alongside zone dimensions in the layout editor overlay. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- drawing.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/drawing.js b/drawing.js index 7dd2692..9d12f7f 100644 --- a/drawing.js +++ b/drawing.js @@ -104,6 +104,7 @@ function drawLayout(cr, node, displayRect, colors = DefaultColors, cornerRadius // Draw zone number and size (width x height in pixels) - use regionRect for live updates const sizeText = `${Math.round(regionRect.width)}x${Math.round(regionRect.height)}`; + const gapText = `Gap: ${node.margin}px`; // Standard uniform font sizes - scale down only if zone is too small const baseNumberSize = 72; // doubled for 4K visibility @@ -123,11 +124,12 @@ function drawLayout(cr, node, displayRect, colors = DefaultColors, cornerRadius cr.setFontSize(sizeFontSize); const sizeExtents = cr.textExtents(sizeText); + const gapExtents = cr.textExtents(gapText); // Calculate center position const centerX = regionRect.x + regionRect.width / 2; const centerY = regionRect.y + regionRect.height / 2; - const totalHeight = numberExtents.height + sizeExtents.height + 15; // 15px spacing (increased) + const totalHeight = numberExtents.height + sizeExtents.height + gapExtents.height + 25; // spacing for 3 lines // Draw zone number with shadow for readability (centered, larger) cr.setFontSize(numberFontSize); @@ -158,6 +160,21 @@ function drawLayout(cr, node, displayRect, colors = DefaultColors, cornerRadius cr.setSourceRGBA(colors.border.r, colors.border.g, colors.border.b, 1); cr.moveTo(sizeX, sizeY); cr.showText(sizeText); + + // Draw gap text with shadow for readability (centered, smaller, below size) + cr.setFontSize(sizeFontSize); + const gapX = centerX - gapExtents.width / 2; + const gapY = sizeY + gapExtents.height + 10; // 10px below size text + + // Shadow/outline for gap text + cr.setSourceRGBA(0, 0, 0, 0.8); // dark shadow + cr.moveTo(gapX + 2, gapY + 2); + cr.showText(gapText); + + // Main gap text + cr.setSourceRGBA(colors.border.r, colors.border.g, colors.border.b, 1); + cr.moveTo(gapX, gapY); + cr.showText(gapText); } for (let child of node.children) {