Skip to content

Commit

Permalink
Merge branch 'development' into js-layouts
Browse files Browse the repository at this point in the history
* development:
  Fix swapping with two windows visible for two-pane layout (#1241)
  Add 4Column layout for ultra-wide screens (#1154)
  Throw window to any space across multiple screens. (#1209)
  Make TwoPane layout adaptive to screen orientation (#1182)
  Minor fix (line 69) (#1230)
  Bump cocoapods-downloader from 1.5.1 to 1.6.3 (#1226)
  Fix the layout HUD when multiple screen are arranged vertically (#1162) (#1163)
  • Loading branch information
ianyh committed May 21, 2022
2 parents d2358e1 + 2b3432b commit aa2cc5e
Show file tree
Hide file tree
Showing 13 changed files with 583 additions and 20 deletions.
4 changes: 4 additions & 0 deletions Amethyst.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
AA4AF40D26717DA900D2AE1B /* TwoPaneLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4AF40C26717DA900D2AE1B /* TwoPaneLayout.swift */; };
AAAC6BAC2677DF7B00BEC1B0 /* TwoPaneLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAC6BAB2677DF7B00BEC1B0 /* TwoPaneLayoutTests.swift */; };
ED989E6BAE0E8D035277478A /* Pods_Amethyst.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6611384054CC1D59E32FC049 /* Pods_Amethyst.framework */; };
F46629C4272AD7A30040C275 /* FourColumnLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46629C3272AD7A30040C275 /* FourColumnLayout.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -247,6 +248,7 @@
A6102745A914DC1AD00462D0 /* Pods-Amethyst.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Amethyst.release.xcconfig"; path = "Target Support Files/Pods-Amethyst/Pods-Amethyst.release.xcconfig"; sourceTree = "<group>"; };
AA4AF40C26717DA900D2AE1B /* TwoPaneLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoPaneLayout.swift; sourceTree = "<group>"; };
AAAC6BAB2677DF7B00BEC1B0 /* TwoPaneLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoPaneLayoutTests.swift; sourceTree = "<group>"; };
F46629C3272AD7A30040C275 /* FourColumnLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FourColumnLayout.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -345,6 +347,7 @@
AA4AF40C26717DA900D2AE1B /* TwoPaneLayout.swift */,
4062AD301C1FA29600DB612B /* TallRightLayout.swift */,
4493EAA12139D9EF00AA9623 /* ThreeColumnLayout.swift */,
F46629C3272AD7A30040C275 /* FourColumnLayout.swift */,
4062AD341C1FA62500DB612B /* WideLayout.swift */,
4062AD3A1C206EA900DB612B /* WidescreenTallLayout.swift */,
4045416E268FFDA000861BE8 /* CustomLayout.swift */,
Expand Down Expand Up @@ -842,6 +845,7 @@
2A6D9A4125E5D24D006A36B5 /* AppManager.swift in Sources */,
4046EFCF2236019400113067 /* Window.swift in Sources */,
40A87FDC2404AEAD005EE9C6 /* main.swift in Sources */,
F46629C4272AD7A30040C275 /* FourColumnLayout.swift in Sources */,
401BC8A61CE8E65D00F89B3F /* LayoutNameWindow.swift in Sources */,
401C35AF2241BBDD0019ED07 /* ReflowOperation.swift in Sources */,
401BC8B41CE9250800F89B3F /* HotKeyRegistrar.swift in Sources */,
Expand Down
291 changes: 291 additions & 0 deletions Amethyst/Layout/Layouts/FourColumnLayout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
//
// FourColumnLayout.swift
// Amethyst
//
// Originally created by Ian Ynda-Hummel on 12/15/15.
// Copyright © 2015 Ian Ynda-Hummel. All rights reserved.
//
// Modifications by Craig Disselkoen on 09/03/18.
// Modifications by Reyk Floeter on 10/28/21.
//

import Silica

// we'd like to hide these structures and enums behind fileprivate, but
// https://bugs.swift.org/browse/SR-47

enum FourColumn {
case left
case middleLeft
case middleRight
case right
}

enum FourPane {
case main
case secondary
case tertiary
case quaternary
}

struct FourPaneWidths {
var left: CGFloat = 0
var middleLeft: CGFloat = 0
var middleRight: CGFloat = 0
var right: CGFloat = 0
}

struct QuadruplePaneArrangement {
/// number of windows in pane
private let paneCount: [FourPane: UInt]

/// height of windows in pane
private let paneWindowHeight: [FourPane: CGFloat]

/// width of windows in pane
private let paneWindowWidth: [FourPane: CGFloat]

// how panes relate to columns
private let panePosition: [FourPane: FourColumn]

/// how columns relate to panes
private let columnDesignation: [FourColumn: FourPane]

/**
- Parameters:
- mainPane: which Column is the main Pane
- numWindows: how many windows total
- numMainPane: how many windows in the main Pane
- screenSize: total size of the screen
- mainPaneRatio: ratio of the screen taken by main pane
*/
init(mainPane: FourColumn, numWindows: UInt, numMainPane: UInt, screenSize: CGSize, mainPaneRatio: CGFloat) {
// forward and reverse mapping of columns to their designations
self.panePosition = {
switch mainPane {
case .left: return [.main: .left, .secondary: .middleLeft, .tertiary: .middleRight, .quaternary: .right]
case .middleLeft: return [.main: .middleLeft, .secondary: .middleRight, .tertiary: .left, .quaternary: .right]
case .middleRight: return [.main: .middleRight, .secondary: .middleLeft, .tertiary: .right, .quaternary: .left]
case .right: return [.main: .right, .secondary: .middleRight, .tertiary: .middleLeft, .quaternary: .left]
}
}()
// swap keys and values for reverse lookup
self.columnDesignation = Dictionary(uniqueKeysWithValues: panePosition.map({ ($1, $0) }))

// calculate how many are in each type
let mainPaneCount = min(numWindows, numMainPane)
let nonMainCount: UInt = numWindows - mainPaneCount
// we do tertiary first because a single window produces a zero in integer division by 2
let nonMainPaneCount: UInt = max(nonMainCount / 3, 1)
let quaternaryPaneCount = nonMainPaneCount
let tertiaryPaneCount = nonMainPaneCount
let secondaryPaneCount = nonMainPaneCount + max(nonMainCount, 3) % 3
self.paneCount = [.main: mainPaneCount, .secondary: secondaryPaneCount, .tertiary: tertiaryPaneCount, .quaternary: quaternaryPaneCount]

// calculate heights
let screenHeight = screenSize.height
self.paneWindowHeight = [
.main: round(screenHeight / CGFloat(mainPaneCount)),
.secondary: secondaryPaneCount == 0 ? 0.0 : round(screenHeight / CGFloat(secondaryPaneCount)),
.tertiary: tertiaryPaneCount == 0 ? 0.0 : round(screenHeight / CGFloat(tertiaryPaneCount)),
.quaternary: quaternaryPaneCount == 0 ? 0.0 : round(screenHeight / CGFloat(quaternaryPaneCount))
]

// calculate widths
let screenWidth = screenSize.width
let mainWindowWidth = round(screenWidth / 4)
let nonMainWindowWidth = round(screenWidth / 4)
self.paneWindowWidth = [
.main: mainWindowWidth,
.secondary: nonMainWindowWidth,
.tertiary: nonMainWindowWidth,
.quaternary: nonMainWindowWidth
]
}

func count(_ pane: FourPane) -> UInt {
return paneCount[pane]!
}

func height(_ pane: FourPane) -> CGFloat {
return paneWindowHeight[pane]!
}

func width(_ pane: FourPane) -> CGFloat {
return paneWindowWidth[pane]!
}

func firstIndex(_ pane: FourPane) -> UInt {
switch pane {
case .main: return 0
case .secondary: return count(.main)
case .tertiary: return count(.main) + count(.secondary)
case .quaternary: return count(.main) + count(.secondary) + count(.tertiary)
}
}

func pane(ofIndex windowIndex: UInt) -> FourPane {
if windowIndex >= firstIndex(.quaternary) {
return .quaternary
}
if windowIndex >= firstIndex(.tertiary) {
return .tertiary
}
if windowIndex >= firstIndex(.secondary) {
return .secondary
}
return .main
}

/// Given a window index, which Pane does it belong to, and which index within that Pane
func coordinates(at windowIndex: UInt) -> (FourPane, UInt) {
let pane = self.pane(ofIndex: windowIndex)
return (pane, windowIndex - firstIndex(pane))
}

/// Get the (height, width) dimensions for a window in the given Pane
func windowDimensions(inPane pane: FourPane) -> (CGFloat, CGFloat) {
return (height(pane), width(pane))
}

/// Get the Column assignment for the given Pane
func column(ofPane pane: FourPane) -> FourColumn {
return panePosition[pane]!
}

func pane(ofColumn column: FourColumn) -> FourPane {
return columnDesignation[column]!
}

/// Get the column widths in the order (left, middle, right)
func widthsLeftToRight() -> FourPaneWidths {
return FourPaneWidths(
left: width(pane(ofColumn: .left)),
middleLeft: width(pane(ofColumn: .middleLeft)),
middleRight: width(pane(ofColumn: .middleRight)),
right: width(pane(ofColumn: .right))
)
}
}

// not an actual Layout, just a base class for the four actual Layouts below
class FourColumnLayout<Window: WindowType>: Layout<Window> {
class var mainColumn: FourColumn { fatalError("Must be implemented by subclass") }

enum CodingKeys: String, CodingKey {
case mainPaneCount
case mainPaneRatio
}

private(set) var mainPaneCount: Int = 1
private(set) var mainPaneRatio: CGFloat = 0.5

required init() {
super.init()
}

required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.mainPaneCount = try values.decode(Int.self, forKey: .mainPaneCount)
self.mainPaneRatio = try values.decode(CGFloat.self, forKey: .mainPaneRatio)
super.init()
}

override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(mainPaneCount, forKey: .mainPaneCount)
try container.encode(mainPaneRatio, forKey: .mainPaneRatio)
}

override func frameAssignments(_ windowSet: WindowSet<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
let windows = windowSet.windows

guard !windows.isEmpty else {
return []
}

let screenFrame = screen.adjustedFrame()
let paneArrangement = QuadruplePaneArrangement(
mainPane: type(of: self).mainColumn,
numWindows: UInt(windows.count),
numMainPane: UInt(mainPaneCount),
screenSize: screenFrame.size,
mainPaneRatio: mainPaneRatio
)

return windows.reduce([]) { frameAssignments, window -> [FrameAssignmentOperation<Window>] in
var assignments = frameAssignments
var windowFrame = CGRect.zero
let windowIndex: UInt = UInt(frameAssignments.count)

let (pane, paneIndex) = paneArrangement.coordinates(at: windowIndex)

let (windowHeight, windowWidth): (CGFloat, CGFloat) = paneArrangement.windowDimensions(inPane: pane)
let column: FourColumn = paneArrangement.column(ofPane: pane)

let widths = paneArrangement.widthsLeftToRight()

let xorigin: CGFloat = screenFrame.origin.x + {
switch column {
case .left: return 0.0
case .middleLeft: return widths.left
case .middleRight: return widths.left + widths.middleLeft
case .right: return widths.left + widths.middleLeft + widths.middleRight
}
}()

let scaleFactor: CGFloat = screenFrame.width / {
if pane == .main {
return paneArrangement.width(.main)
}
return paneArrangement.width(.secondary) + paneArrangement.width(.tertiary) + paneArrangement.width(.quaternary)
}()

windowFrame.origin.x = xorigin
windowFrame.origin.y = screenFrame.origin.y + (windowHeight * CGFloat(paneIndex))
windowFrame.size.width = windowWidth
windowFrame.size.height = windowHeight

let isMain = windowIndex < paneArrangement.firstIndex(.secondary)

let resizeRules = ResizeRules(isMain: isMain, unconstrainedDimension: .horizontal, scaleFactor: scaleFactor)
let frameAssignment = FrameAssignment<Window>(
frame: windowFrame,
window: window,
screenFrame: screenFrame,
resizeRules: resizeRules
)

assignments.append(FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet))

return assignments
}
}
}

extension FourColumnLayout {
func recommendMainPaneRawRatio(rawRatio: CGFloat) {
mainPaneRatio = rawRatio
}

func increaseMainPaneCount() {
mainPaneCount += 1
}

func decreaseMainPaneCount() {
mainPaneCount = max(1, mainPaneCount - 1)
}
}

// implement the two variants
class FourColumnLeftLayout<Window: WindowType>: FourColumnLayout<Window>, PanedLayout {
override static var layoutName: String { return "4Column Left" }
override static var layoutKey: String { return "4column-left" }
override static var mainColumn: FourColumn { return .middleLeft }
}

class FourColumnRightLayout<Window: WindowType>: FourColumnLayout<Window>, PanedLayout {
override static var layoutName: String { return "4Column Right" }
override static var layoutKey: String { return "4column-right" }
override static var mainColumn: FourColumn { return .middleRight }
}
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
12 changes: 12 additions & 0 deletions Amethyst/Managers/LayoutType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ enum LayoutType<Window: WindowType> {
case threeColumnLeft
case threeColumnMiddle
case threeColumnRight
case fourColumnLeft
case fourColumnRight
case fullscreen
case column
case row
Expand All @@ -68,6 +70,8 @@ enum LayoutType<Window: WindowType> {
.threeColumnLeft,
.threeColumnMiddle,
.threeColumnRight,
.fourColumnLeft,
.fourColumnRight,
.fullscreen,
.column,
.row,
Expand All @@ -94,6 +98,10 @@ enum LayoutType<Window: WindowType> {
return "middle-wide"
case .threeColumnRight:
return "3column-right"
case .fourColumnLeft:
return "4column-left"
case .fourColumnRight:
return "4column-right"
case .fullscreen:
return "fullscreen"
case .column:
Expand Down Expand Up @@ -129,6 +137,10 @@ enum LayoutType<Window: WindowType> {
return ThreeColumnMiddleLayout<Window>.self
case .threeColumnRight:
return ThreeColumnRightLayout<Window>.self
case .fourColumnLeft:
return FourColumnLeftLayout<Window>.self
case .fourColumnRight:
return FourColumnRightLayout<Window>.self
case .fullscreen:
return FullscreenLayout<Window>.self
case .column:
Expand Down
Loading

0 comments on commit aa2cc5e

Please sign in to comment.