Skip to content

Commit

Permalink
Make TwoPane layout adaptive to screen orientation (#1182)
Browse files Browse the repository at this point in the history
* Make Two Pane view adaptive to screen orientation

* add tests
  • Loading branch information
mwz authored May 7, 2022
1 parent 64609ea commit 45444d8
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 8 deletions.
15 changes: 8 additions & 7 deletions Amethyst/Layout/Layouts/TwoPaneLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,13 @@ class TwoPaneLayout<Window: WindowType>: Layout<Window>, 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<Window>] in
var assignments = acc
Expand All @@ -75,13 +76,13 @@ class TwoPaneLayout<Window: WindowType>: Layout<Window>, 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
}
Expand Down
186 changes: 185 additions & 1 deletion AmethystTests/Tests/Layout/TwoPaneLayoutTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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<TestWindow>(id: $0.id(), frame: $0.frame(), isFocused: false)
}
let windowSet = WindowSet<TestWindow>(
windows: layoutWindows,
isWindowWithIDActive: { _ in return true },
isWindowWithIDFloating: { _ in return false },
windowForID: { id in return windows.first { $0.id() == id } }
)
let layout = TwoPaneLayout<TestWindow>()
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<TestWindow>(id: $0.id(), frame: $0.frame(), isFocused: false)
}
let windowSet = WindowSet<TestWindow>(
windows: layoutWindows,
isWindowWithIDActive: { _ in return true },
isWindowWithIDFloating: { _ in return false },
windowForID: { id in return windows.first { $0.id() == id } }
)
let layout = TwoPaneLayout<TestWindow>()
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<TestWindow>(id: $0.id(), frame: $0.frame(), isFocused: false)
}
let windowSet = WindowSet<TestWindow>(
windows: layoutWindows,
isWindowWithIDActive: { _ in return true },
isWindowWithIDFloating: { _ in return false },
windowForID: { id in return windows.first { $0.id() == id } }
)
let layout = TwoPaneLayout<TestWindow>()

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<TestWindow>(id: $0.id(), frame: $0.frame(), isFocused: false)
}
let windowSet = WindowSet<TestWindow>(
windows: layoutWindows,
isWindowWithIDActive: { _ in return true },
isWindowWithIDFloating: { _ in return false },
windowForID: { id in return windows.first { $0.id() == id } }
)
let layout = TwoPaneLayout<TestWindow>()

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<TestWindow>()
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 45444d8

Please sign in to comment.