From 45444d8ddfc3dcbe6a4b81d4a00d3391827a9259 Mon Sep 17 00:00:00 2001 From: Michael Wizner Date: Sat, 7 May 2022 02:50:34 +0100 Subject: [PATCH] Make TwoPane layout adaptive to screen orientation (#1182) * Make Two Pane view adaptive to screen orientation * add tests --- Amethyst/Layout/Layouts/TwoPaneLayout.swift | 15 +- .../Tests/Layout/TwoPaneLayoutTests.swift | 186 +++++++++++++++++- README.md | 10 + 3 files changed, 203 insertions(+), 8 deletions(-) diff --git a/Amethyst/Layout/Layouts/TwoPaneLayout.swift b/Amethyst/Layout/Layouts/TwoPaneLayout.swift index ee750530..046da5ed 100644 --- a/Amethyst/Layout/Layouts/TwoPaneLayout.swift +++ b/Amethyst/Layout/Layouts/TwoPaneLayout.swift @@ -59,12 +59,13 @@ class TwoPaneLayout: Layout, PanedLayout { let hasSecondaryPane = secondaryPaneCount > 0 let screenFrame = screen.adjustedFrame() + let isHorizontal = (screenFrame.size.width / screenFrame.size.height) >= 1 - let mainPaneWindowHeight = round(screenFrame.size.height / CGFloat(mainPaneCount)) - let secondaryPaneWindowHeight = hasSecondaryPane ? round(screenFrame.size.height / CGFloat(secondaryPaneCount)) : 0.0 + let mainPaneWindowHeight = screenFrame.size.height * (!isHorizontal && hasSecondaryPane ? mainPaneRatio : 1) + let secondaryPaneWindowHeight = isHorizontal ? mainPaneWindowHeight : screenFrame.size.height - mainPaneWindowHeight - let mainPaneWindowWidth = round(screenFrame.size.width * (hasSecondaryPane ? CGFloat(mainPaneRatio) : 1.0)) - let secondaryPaneWindowWidth = screenFrame.size.width - mainPaneWindowWidth + let mainPaneWindowWidth = screenFrame.size.width * (isHorizontal && hasSecondaryPane ? mainPaneRatio : 1) + let secondaryPaneWindowWidth = !isHorizontal ? mainPaneWindowWidth : screenFrame.size.width - mainPaneWindowWidth return windows.reduce([]) { acc, window -> [FrameAssignmentOperation] in var assignments = acc @@ -75,13 +76,13 @@ class TwoPaneLayout: Layout, PanedLayout { if isMain { scaleFactor = screenFrame.size.width / mainPaneWindowWidth windowFrame.origin.x = screenFrame.origin.x - windowFrame.origin.y = screenFrame.origin.y + (mainPaneWindowHeight * CGFloat(acc.count)) + windowFrame.origin.y = screenFrame.origin.y windowFrame.size.width = mainPaneWindowWidth windowFrame.size.height = mainPaneWindowHeight } else { scaleFactor = screenFrame.size.width / secondaryPaneWindowWidth - windowFrame.origin.x = screenFrame.origin.x + mainPaneWindowWidth - windowFrame.origin.y = screenFrame.origin.y + windowFrame.origin.x = screenFrame.origin.x + (isHorizontal ? mainPaneWindowWidth : 0) + windowFrame.origin.y = screenFrame.origin.y + (isHorizontal ? 0 : mainPaneWindowHeight) windowFrame.size.width = secondaryPaneWindowWidth windowFrame.size.height = secondaryPaneWindowHeight } diff --git a/AmethystTests/Tests/Layout/TwoPaneLayoutTests.swift b/AmethystTests/Tests/Layout/TwoPaneLayoutTests.swift index 41f595f6..616a003e 100644 --- a/AmethystTests/Tests/Layout/TwoPaneLayoutTests.swift +++ b/AmethystTests/Tests/Layout/TwoPaneLayoutTests.swift @@ -17,7 +17,7 @@ class TwoPaneLayoutTests: QuickSpec { TestScreen.availableScreens = [] } - describe("layout") { + describe("layout horizontal") { it("separates into a main pane and a secondary pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] @@ -201,6 +201,190 @@ class TwoPaneLayoutTests: QuickSpec { } } + describe("layout vertical") { + it("separates into a main pane and a secondary pane") { + let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 1000, height: 2000))) + TestScreen.availableScreens = [screen] + + let windows = [ + TestWindow(element: nil)!, + TestWindow(element: nil)!, + TestWindow(element: nil)! + ] + let layoutWindows = windows.map { + LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) + } + let windowSet = WindowSet( + windows: layoutWindows, + isWindowWithIDActive: { _ in return true }, + isWindowWithIDFloating: { _ in return false }, + windowForID: { id in return windows.first { $0.id() == id } } + ) + let layout = TwoPaneLayout() + let frameAssignments = layout.frameAssignments(windowSet, on: screen)! + + expect(layout.mainPaneCount).to(equal(1)) + + let mainAssignment = frameAssignments.forWindows(windows[..<1]) + let secondaryAssignments = frameAssignments.forWindows(windows[1...]) + + mainAssignment.verify(frames: [ + CGRect(origin: .zero, size: CGSize(width: 1000, height: 1000)) + ]) + secondaryAssignments.verify(frames: [ + CGRect(x: 0, y: 1000, width: 1000, height: 1000), + CGRect(x: 0, y: 1000, width: 1000, height: 1000) + ]) + } + + it("handles non-origin screen") { + let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 1000, height: 2000)) + TestScreen.availableScreens = [screen] + + let windows = [ + TestWindow(element: nil)!, + TestWindow(element: nil)!, + TestWindow(element: nil)! + ] + let layoutWindows = windows.map { + LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) + } + let windowSet = WindowSet( + windows: layoutWindows, + isWindowWithIDActive: { _ in return true }, + isWindowWithIDFloating: { _ in return false }, + windowForID: { id in return windows.first { $0.id() == id } } + ) + let layout = TwoPaneLayout() + let frameAssignments = layout.frameAssignments(windowSet, on: screen)! + + expect(layout.mainPaneCount).to(equal(1)) + + let mainAssignment = frameAssignments.forWindows(windows[..<1]) + let secondaryAssignments = frameAssignments.forWindows(windows[1...]) + + mainAssignment.verify(frames: [ + CGRect(x: 100, y: 100, width: 1000, height: 1000) + ]) + secondaryAssignments.verify(frames: [ + CGRect(x: 100, y: 1100, width: 1000, height: 1000), + CGRect(x: 100, y: 1100, width: 1000, height: 1000) + ]) + } + + it("does not increase and decrease windows in the main pane") { + let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 1000, height: 2000))) + TestScreen.availableScreens = [screen] + + let windows = [ + TestWindow(element: nil)!, + TestWindow(element: nil)!, + TestWindow(element: nil)!, + TestWindow(element: nil)! + ] + let layoutWindows = windows.map { + LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) + } + let windowSet = WindowSet( + windows: layoutWindows, + isWindowWithIDActive: { _ in return true }, + isWindowWithIDFloating: { _ in return false }, + windowForID: { id in return windows.first { $0.id() == id } } + ) + let layout = TwoPaneLayout() + + expect(layout.mainPaneCount).to(equal(1)) + + let frameAssignments = layout.frameAssignments(windowSet, on: screen)! + let mainAssignments = frameAssignments.forWindows(windows[..<1]) + let secondaryAssignments = frameAssignments.forWindows(windows[1...]) + + mainAssignments.verify(frames: [ + CGRect(x: 0, y: 0, width: 1000, height: 1000) + ]) + + secondaryAssignments.verify(frames: [ + CGRect(x: 0, y: 1000, width: 1000, height: 1000), + CGRect(x: 0, y: 1000, width: 1000, height: 1000), + CGRect(x: 0, y: 1000, width: 1000, height: 1000) + ]) + + layout.increaseMainPaneCount() + expect(layout.mainPaneCount).to(equal(1)) + + layout.decreaseMainPaneCount() + expect(layout.mainPaneCount).to(equal(1)) + } + + it("changes distribution based on pane ratio") { + let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 1000, height: 2000))) + TestScreen.availableScreens = [screen] + + let windows = [ + TestWindow(element: nil)!, + TestWindow(element: nil)!, + TestWindow(element: nil)! + ] + let layoutWindows = windows.map { + LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) + } + let windowSet = WindowSet( + windows: layoutWindows, + isWindowWithIDActive: { _ in return true }, + isWindowWithIDFloating: { _ in return false }, + windowForID: { id in return windows.first { $0.id() == id } } + ) + let layout = TwoPaneLayout() + + expect(layout.mainPaneCount).to(equal(1)) + + var frameAssignments = layout.frameAssignments(windowSet, on: screen)! + var mainAssignments = frameAssignments.forWindows(windows[..<1]) + var secondaryAssignments = frameAssignments.forWindows(windows[1...]) + + mainAssignments.verify(frames: [ + CGRect(x: 0, y: 0, width: 1000, height: 1000) + ]) + + secondaryAssignments.verify(frames: [ + CGRect(x: 0, y: 1000, width: 1000, height: 1000), + CGRect(x: 0, y: 1000, width: 1000, height: 1000) + ]) + + layout.recommendMainPaneRatio(0.75) + expect(layout.mainPaneRatio).to(equal(0.75)) + + frameAssignments = layout.frameAssignments(windowSet, on: screen)! + mainAssignments = frameAssignments.forWindows(windows[..<1]) + secondaryAssignments = frameAssignments.forWindows(windows[1...]) + + mainAssignments.verify(frames: [ + CGRect(x: 0, y: 0, width: 1000, height: 1500) + ]) + + secondaryAssignments.verify(frames: [ + CGRect(x: 0, y: 1500, width: 1000, height: 500), + CGRect(x: 0, y: 1500, width: 1000, height: 500) + ]) + + layout.recommendMainPaneRatio(0.25) + expect(layout.mainPaneRatio).to(equal(0.25)) + + frameAssignments = layout.frameAssignments(windowSet, on: screen)! + mainAssignments = frameAssignments.forWindows(windows[..<1]) + secondaryAssignments = frameAssignments.forWindows(windows[1...]) + + mainAssignments.verify(frames: [ + CGRect(x: 0, y: 0, width: 1000, height: 500) + ]) + + secondaryAssignments.verify(frames: [ + CGRect(x: 0, y: 500, width: 1000, height: 1500), + CGRect(x: 0, y: 500, width: 1000, height: 1500) + ]) + } + } + describe("coding") { it("encodes and decodes") { let layout = TwoPaneLayout() diff --git a/README.md b/README.md index 360f5b1a..dd61ae08 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,16 @@ Exactly the same as *Tall*, but the main pane is on the right, with the other pa The rotated version of *Tall*, where the main pane is on the _top_ (extending the full width of the screen), and the other pane is on the bottom. If either pane has more than one window, that pane will split into columns instead of rows. +#### Two Pane + +This layout has two visible panes - the main and the secondary pane. The window +in the main pane is pinned, just like in other layouts, and all the remaining +windows are placed in the other pane with only one window being visible at a +time, which can be swapped (using the keyboard shortcuts). This layout +automatically adapts to horizontal/vertical tiling depending on your screen +orientation. The main pane is on the left in the horizontal orientation and it's +on the top in the vertical orientation. + #### 3Column-Left A three-column version of *Tall*, with one main pane on the left (extending the full height of the screen) and two other panes, one in the middle and one on the right. Like *Tall*, if any pane has more than one window, that pane will be split into rows. You can control how many windows are in the main pane as usual; other windows will be assigned as evenly as possible between the other two panes.