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
8 changes: 6 additions & 2 deletions Amethyst.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
40111CC9223370FD003D20BD /* SIWindow+AmethystTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40111CC8223370FD003D20BD /* SIWindow+AmethystTests.swift */; };
40111CCB22342CC4003D20BD /* DebugPreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40111CCA22342CC4003D20BD /* DebugPreferencesViewController.swift */; };
40111CCD22342CF3003D20BD /* DebugPreferencesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 40111CCC22342CF3003D20BD /* DebugPreferencesViewController.xib */; };
401ADBAF2D55A1B6001FF53A /* recommended-main-pane-ratio.js in Resources */ = {isa = PBXBuildFile; fileRef = 401ADBAE2D55A1B6001FF53A /* recommended-main-pane-ratio.js */; };
401BBCB62333067F005118F8 /* ColumnLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BBCB52333067F005118F8 /* ColumnLayoutTests.swift */; };
401BC8981CE7E45300F89B3F /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BC8971CE7E45300F89B3F /* WindowManager.swift */; };
401BC89A1CE8C6AE00F89B3F /* HotKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BC8991CE8C6AE00F89B3F /* HotKeyManager.swift */; };
Expand Down Expand Up @@ -153,6 +154,7 @@
40111CCA22342CC4003D20BD /* DebugPreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugPreferencesViewController.swift; sourceTree = "<group>"; };
40111CCC22342CF3003D20BD /* DebugPreferencesViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DebugPreferencesViewController.xib; sourceTree = "<group>"; };
401A529824D3B63A004359A4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
401ADBAE2D55A1B6001FF53A /* recommended-main-pane-ratio.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "recommended-main-pane-ratio.js"; sourceTree = "<group>"; };
401BBCB52333067F005118F8 /* ColumnLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnLayoutTests.swift; sourceTree = "<group>"; };
401BC8971CE7E45300F89B3F /* WindowManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = "<group>"; };
401BC8991CE8C6AE00F89B3F /* HotKeyManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HotKeyManager.swift; path = ../Events/HotKeyManager.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -459,12 +461,13 @@
404541722697C16B00861BE8 /* CustomLayouts */ = {
isa = PBXGroup;
children = (
4087BB032D447F480062A52B /* static-ratio-tall-native-commands.js */,
40D82FF029739C5300F3C18B /* extended.js */,
40DA8B6D27D5AA7300C291AF /* subset.js */,
404541772697CDD000861BE8 /* fullscreen.js */,
404541732697C22A00861BE8 /* null.js */,
401ADBAE2D55A1B6001FF53A /* recommended-main-pane-ratio.js */,
4045417D2698030A00861BE8 /* static-ratio-tall.js */,
4087BB032D447F480062A52B /* static-ratio-tall-native-commands.js */,
40DA8B6D27D5AA7300C291AF /* subset.js */,
404541792697EBC500861BE8 /* undefined.js */,
4045417B2697EE7800861BE8 /* uniform-columns.js */,
);
Expand Down Expand Up @@ -748,6 +751,7 @@
4045417C2697EE7800861BE8 /* uniform-columns.js in Resources */,
40DA8B6E27D5AA7300C291AF /* subset.js in Resources */,
40D82FF129739C5300F3C18B /* extended.js in Resources */,
401ADBAF2D55A1B6001FF53A /* recommended-main-pane-ratio.js in Resources */,
404541782697CDD000861BE8 /* fullscreen.js in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
8 changes: 5 additions & 3 deletions Amethyst/Layout/Layout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,11 @@ extension Layout {
- Note: This does not necessarily correspond to the final frame of the window as windows do not necessarily take the exact frame the layout provides.
*/
func assignedFrame(_ window: Window, of windowSet: WindowSet<Window>, on screen: Screen) -> FrameAssignment<Window>? {
return frameAssignments(windowSet, on: screen)?
.map { $0.frameAssignment }
.first { $0.window.id == window.id() }
guard let assignments = frameAssignments(windowSet, on: screen) else {
return nil
}

return assignments.map { $0.frameAssignment }.first { $0.window.id == window.id() }
}
}

Expand Down
79 changes: 63 additions & 16 deletions Amethyst/Layout/Layouts/CustomLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class CustomLayout<Window: WindowType>: StatefulLayout<Window>, PanedLayout {

private lazy var context: JSContext? = {
guard let context = JSContext() else {
log.error("Failed to create javascript context")
return nil
}

Expand All @@ -65,6 +66,7 @@ class CustomLayout<Window: WindowType>: StatefulLayout<Window>, PanedLayout {
}

context.evaluateScript("var console = { log: function(message) { _consoleLog(message) } }")

let consoleLog: @convention(block) (String) -> Void = { message in
log.debug(message)
}
Expand All @@ -73,14 +75,30 @@ class CustomLayout<Window: WindowType>: StatefulLayout<Window>, PanedLayout {
do {
context.evaluateScript(try String(contentsOf: self.fileURL))
} catch {
log.error(error)
return nil
}

context.evaluateScript("""
function sanitizeArguments(fn) {
return function(...args) {
const sanitizedArgs = args.map(arg => !!arg ? JSON.parse(JSON.stringify(arg)) : undefined);
return fn(...sanitizedArgs);
};
}

function normalizedLayout() {
const l = layout();
l.getFrameAssignments = sanitizeArguments(l.getFrameAssignments);
return l;
}
""")

return context
}()

private lazy var layout: JSValue? = {
return self.context?.objectForKeyedSubscript("layout")?.call(withArguments: [])
return self.context?.objectForKeyedSubscript("normalizedLayout")?.call(withArguments: [])
}()

private lazy var state: JSValue? = {
Expand Down Expand Up @@ -136,13 +154,6 @@ class CustomLayout<Window: WindowType>: StatefulLayout<Window>, PanedLayout {
}

override func frameAssignments(_ windowSet: WindowSet<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
guard
let getFrameAssignments = layout?.objectForKeyedSubscript("getFrameAssignments"),
!getFrameAssignments.isNull && !getFrameAssignments.isUndefined
else {
return nil
}

let windows = windowSet.windows

guard !windows.isEmpty else {
Expand Down Expand Up @@ -183,21 +194,43 @@ class CustomLayout<Window: WindowType>: StatefulLayout<Window>, PanedLayout {
extendedFrames ?? JSValue(undefinedIn: context)!
]

guard
let frameAssignmentsValue = getFrameAssignments.call(withArguments: args),
frameAssignmentsValue.isObject
else {
guard let getAssignments = layout?.objectForKeyedSubscript("getFrameAssignments"), !getAssignments.isNull && !getAssignments.isUndefined else {
return nil
}

guard let assignments = getAssignments.call(withArguments: args), assignments.isObject else {
return nil
}

let resizeRules = ResizeRules(isMain: true, unconstrainedDimension: .horizontal, scaleFactor: 1)
return jsWindows.values.compactMap { jsWindow in
guard let frame = frameAssignmentsValue.objectForKeyedSubscript(jsWindow.id)?.toRoundedRect() else {
return windows.compactMap { window -> FrameAssignmentOperation<Window>? in
guard let jsWindow = jsWindows[window.id] else {
return nil
}

guard let frame = assignments.objectForKeyedSubscript(jsWindow.id) else {
return nil
}

var unconstrainedDimension: UnconstrainedDimension = .horizontal
var scaleFactor = screenFrame.width / frame.toRoundedRect().width

if let dimension = frame.objectForKeyedSubscript("unconstrainedDimension")?.toString() {
switch dimension {
case "horizontal":
unconstrainedDimension = .horizontal
case "vertical":
unconstrainedDimension = .vertical
scaleFactor = screenFrame.height / frame.toRoundedRect().height
default:
log.warning("Encountered unknown unconstrainedDimension value: \(dimension), defaulting to horizontal")
unconstrainedDimension = .horizontal
}
}

let isMain = frame.objectForKeyedSubscript("isMain")?.toBool() ?? true
let resizeRules = ResizeRules(isMain: isMain, unconstrainedDimension: unconstrainedDimension, scaleFactor: scaleFactor)
let frameAssignment = FrameAssignment<Window>(
frame: frame,
frame: frame.toRoundedRect(),
window: jsWindow.window,
screenFrame: screenFrame,
resizeRules: resizeRules
Expand Down Expand Up @@ -329,7 +362,21 @@ class CustomLayout<Window: WindowType>: StatefulLayout<Window>, PanedLayout {
}

func recommendMainPaneRawRatio(rawRatio: CGFloat) {
guard
let recommendMainPaneRatio = layout?.objectForKeyedSubscript("recommendMainPaneRatio"),
!recommendMainPaneRatio.isNull && !recommendMainPaneRatio.isUndefined
else {
return
}

let recommendMainPaneRatioArgs: [Any]? = state.flatMap { [rawRatio, $0] }

guard let updatedState = recommendMainPaneRatio.call(withArguments: recommendMainPaneRatioArgs ?? []), !updatedState.isNull && !updatedState.isUndefined else {
log.error("\(layoutKey) — recommendMainPaneRawRatio: received invalid updated state")
return
}

state = updatedState
}

func increaseMainPaneCount() {
Expand Down
36 changes: 36 additions & 0 deletions AmethystTests/Model/CustomLayouts/recommended-main-pane-ratio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
function layout() {
return {
name: "Ratio",
initialState: {
mainPaneRatio: 0.5
},
recommendMainPaneRatio: (ratio, state) => {
return { ...state, mainPaneRatio: ratio };
},
getFrameAssignments: (windows, screenFrame, state) => {
return windows.reduce((frames, window, index) => {
if (index === 0) {
const frame = {
x: screenFrame.x,
y: screenFrame.y,
width: screenFrame.width * state.mainPaneRatio,
height: screenFrame.height,
isMain: true,
unconstrainedDimension: "horizontal"
};
return { ...frames, [window.id]: frame };
} else {
const frame = {
x: screenFrame.x + screenFrame.width * state.mainPaneRatio,
y: screenFrame.y,
width: screenFrame.width - screenFrame.width * state.mainPaneRatio,
height: screenFrame.height,
isMain: false,
unconstrainedDimension: "horizontal"
};
return { ...frames, [window.id]: frame };
}
}, {});
}
};
}
39 changes: 39 additions & 0 deletions AmethystTests/Tests/Layout/CustomLayoutTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -695,5 +695,44 @@ class CustomLayoutTests: QuickSpec {
}
}
}

describe("recommend main pane ratio") {
it("receives correct ratio") {
let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000)))
TestScreen.availableScreens = [screen]

let window = TestWindow(element: nil)!
let layoutWindow = LayoutWindow<TestWindow>(id: window.id(), frame: window.frame(), isFocused: false)
let windowSet = WindowSet<TestWindow>(
windows: [layoutWindow],
isWindowWithIDActive: { _ in return true },
isWindowWithIDFloating: { _ in return false },
windowForID: { _ in window }
)
let layout = CustomLayout<TestWindow>(key: "recommended-main-pane-ratio", fileURL: Bundle.layoutFile(key: "recommended-main-pane-ratio")!)
var frameAssignments = layout.frameAssignments(windowSet, on: screen)!
var mainAssignment = frameAssignments.forWindows([window])

mainAssignment.verify(frames: [
CGRect(x: 0, y: 0, width: 1000, height: 1000)
])

layout.recommendMainPaneRawRatio(rawRatio: 0.25)
frameAssignments = layout.frameAssignments(windowSet, on: screen)!
mainAssignment = frameAssignments.forWindows([window])

mainAssignment.verify(frames: [
CGRect(x: 0, y: 0, width: 500, height: 1000)
])

layout.recommendMainPaneRawRatio(rawRatio: 0.75)
frameAssignments = layout.frameAssignments(windowSet, on: screen)!
mainAssignment = frameAssignments.forWindows([window])

mainAssignment.verify(frames: [
CGRect(x: 0, y: 0, width: 1500, height: 1000)
])
}
}
}
}
18 changes: 17 additions & 1 deletion docs/custom-layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ A function that takes two arguments—`change` and `state`—and must return a n

* `change`: the particular change the layout needs to respond to.

#### `recommendMainPaneRatio`

A function that takes two arguments—`ratio` and `state`—and must return a new layout state based on the recommended ratio.

* `ratio`: the ratio recommended for the layout based on windows being resized by mouse controls.

### Mouse Resizing

Amethyst supports changing the relative ratios of windows when changing the size of windows by dragging them with the cursor. By default, these ratios are recommended by calling the `recommendMainPaneRatio` layout property, and happen on the horizontal axis. When the window is resized, the system determines what ratio is appropriate for the new width given the dimensions of the screen it is on. These values are clamped to [0, 1].

To scale along a different axis, you can specify the `unconstrainedDimension` and `isMain` properties of each window's frame. The dimension determines the axis along which window frame changes will cause recommended ratio changes, and the `isMain` property determines which part of the ratio the window applies to.

Note that currently the recommended ratio is global to the layout and not specific to a given window, so it is not particularly meaningful to specify multiple `unconstrainedDimension` values among frames.

### Common Structures

#### Windows
Expand All @@ -64,12 +78,14 @@ A window is an object with three properties.

#### Frames

A frame is an object with four properties.
A frame is an object with four required properties and two optional properties.

* `x`: x-coordinate in the screen space
* `y`: y-coordinate in the screen space
* `width`: pixel width
* `height`: pixel height
* (optional) `isMain`: boolean indicating whether the window is in the main pane (default: `true`)
* (optional) `unconstrainedDimension`: a string indicating on which axis the window is able to be resized via mouse (values: `horizontal`, `vertical`; default: `horizontal`)

Note that frames are in a global space, not relative to a given screen.

Expand Down
Loading