Skip to content
Open
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
2 changes: 2 additions & 0 deletions Sources/AppBundle/command/cmdManifest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ extension CmdArgs {
command = ReloadConfigCommand(args: self as! ReloadConfigCmdArgs)
case .resize:
command = ResizeCommand(args: self as! ResizeCmdArgs)
case .scroll:
command = ScrollCommand(args: self as! ScrollCmdArgs)
case .split:
command = SplitCommand(args: self as! SplitCmdArgs)
case .subscribe:
Expand Down
30 changes: 20 additions & 10 deletions Sources/AppBundle/command/format.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,20 +167,30 @@ private func toLayoutString(tc: TilingContainer) -> String {
case (.tiles, .v): return LayoutCmdArgs.LayoutDescription.v_tiles.rawValue
case (.accordion, .h): return LayoutCmdArgs.LayoutDescription.h_accordion.rawValue
case (.accordion, .v): return LayoutCmdArgs.LayoutDescription.v_accordion.rawValue
case (.scrolling, _): return LayoutCmdArgs.LayoutDescription.scrolling.rawValue
}
}

private func toLayoutResult(w: Window) -> Result<Primitive, String> {
guard let parent = w.parent else { return .failure("NULL-PARENT") }
return switch getChildParentRelation(child: w, parent: parent) {
case .tiling(let tc): .success(.string(toLayoutString(tc: tc)))
case .floatingWindow: .success(.string(LayoutCmdArgs.LayoutDescription.floating.rawValue))
case .macosNativeFullscreenWindow: .success(.string("macos_native_fullscreen"))
case .macosNativeHiddenAppWindow: .success(.string("macos_native_window_of_hidden_app"))
case .macosNativeMinimizedWindow: .success(.string("macos_native_minimized"))
case .macosPopupWindow: .success(.string("NULL-WINDOW-LAYOUT"))

case .rootTilingContainer: .failure("Not possible")
case .shimContainerRelation: .failure("Window cannot have a shim container relation")
switch getChildParentRelation(child: w, parent: parent) {
case .tiling(let tc):
let rootContainer = w.parentsWithSelf.compactMap { $0 as? TilingContainer }.last
let layoutContainer = rootContainer?.layout == .scrolling ? rootContainer ?? tc : tc
return .success(.string(toLayoutString(tc: layoutContainer)))
case .floatingWindow:
return .success(.string(LayoutCmdArgs.LayoutDescription.floating.rawValue))
case .macosNativeFullscreenWindow:
return .success(.string("macos_native_fullscreen"))
case .macosNativeHiddenAppWindow:
return .success(.string("macos_native_window_of_hidden_app"))
case .macosNativeMinimizedWindow:
return .success(.string("macos_native_minimized"))
case .macosPopupWindow:
return .success(.string("NULL-WINDOW-LAYOUT"))
case .rootTilingContainer:
return .failure("Not possible")
case .shimContainerRelation:
return .failure("Window cannot have a shim container relation")
}
}
4 changes: 4 additions & 0 deletions Sources/AppBundle/command/impl/BalanceSizesCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ struct BalanceSizesCommand: Command {

func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
if target.workspace.rootTilingContainer.layout == .scrolling {
return io.err("balance-sizes command doesn't support the scrolling layout")
}
balance(target.workspace.rootTilingContainer)
return true
}
Expand All @@ -19,6 +22,7 @@ private func balance(_ parent: TilingContainer) {
switch parent.layout {
case .tiles: child.setWeight(parent.orientation, 1)
case .accordion: break // Do nothing
case .scrolling: break
}
if let child = child as? TilingContainer {
balance(child)
Expand Down
15 changes: 15 additions & 0 deletions Sources/AppBundle/command/impl/LayoutCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ struct LayoutCommand: Command {
return changeTilingLayout(io, targetLayout: .accordion, targetOrientation: nil, window: window)
case .tiles:
return changeTilingLayout(io, targetLayout: .tiles, targetOrientation: nil, window: window)
case .scrolling:
return changeTilingLayout(io, targetLayout: .scrolling, targetOrientation: .h, window: window)
case .horizontal:
return changeTilingLayout(io, targetLayout: nil, targetOrientation: .h, window: window)
case .vertical:
Expand Down Expand Up @@ -57,10 +59,21 @@ struct LayoutCommand: Command {
guard let parent = window.parent else { return false }
switch parent.cases {
case .tilingContainer(let parent):
if targetLayout == .scrolling && !parent.isRootContainer {
return io.err("The 'scrolling' layout is only supported for workspace root containers")
}
if parent.layout == .scrolling && targetLayout == nil && targetOrientation == .v {
return io.err("The scrolling layout is always horizontal")
}
let targetOrientation = targetOrientation ?? parent.orientation
let targetLayout = targetLayout ?? parent.layout
parent.layout = targetLayout
parent.changeOrientation(targetOrientation)
if targetLayout == .scrolling {
parent.reveal(window, preferRightPane: true)
} else {
parent.clampScrollingIndex()
}
return true
case .workspace, .macosMinimizedWindowsContainer, .macosFullscreenWindowsContainer,
.macosPopupWindowsContainer, .macosHiddenAppsWindowsContainer:
Expand All @@ -73,6 +86,8 @@ extension Window {
return switch layout {
case .accordion: (parent as? TilingContainer)?.layout == .accordion
case .tiles: (parent as? TilingContainer)?.layout == .tiles
case .scrolling:
parentsWithSelf.compactMap { $0 as? TilingContainer }.last?.layout == .scrolling
case .horizontal: (parent as? TilingContainer)?.orientation == .h
case .vertical: (parent as? TilingContainer)?.orientation == .v
case .h_accordion: (parent as? TilingContainer).map { $0.layout == .accordion && $0.orientation == .h } == true
Expand Down
33 changes: 21 additions & 12 deletions Sources/AppBundle/command/impl/MoveCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,34 @@ struct MoveCommand: Command {
return io.err(noWindowIsFocused)
}
guard let parent = currentWindow.parent else { return false }
let result: Bool
switch parent.cases {
case .tilingContainer(let parent):
let indexOfCurrent = currentWindow.ownIndex.orDie()
let indexOfSiblingTarget = indexOfCurrent + direction.focusOffset
if parent.orientation == direction.orientation && parent.children.indices.contains(indexOfSiblingTarget) {
switch parent.children[indexOfSiblingTarget].tilingTreeNodeCasesOrDie() {
case .tilingContainer(let topLevelSiblingTargetContainer):
return deepMoveIn(window: currentWindow, into: topLevelSiblingTargetContainer, moveDirection: direction)
result = deepMoveIn(window: currentWindow, into: topLevelSiblingTargetContainer, moveDirection: direction)
case .window: // "swap windows"
let prevBinding = currentWindow.unbindFromParent()
currentWindow.bind(to: parent, adaptiveWeight: prevBinding.adaptiveWeight, index: indexOfSiblingTarget)
return true
result = true
}
} else {
return moveOut(window: currentWindow, direction: direction, io, args, env)
result = moveOut(window: currentWindow, direction: direction, io, args, env)
}
case .workspace: // floating window
return io.err("moving floating windows isn't yet supported") // todo
result = io.err("moving floating windows isn't yet supported") // todo
case .macosMinimizedWindowsContainer, .macosFullscreenWindowsContainer, .macosHiddenAppsWindowsContainer:
return io.err(moveOutMacosUnconventionalWindow)
result = io.err(moveOutMacosUnconventionalWindow)
case .macosPopupWindowsContainer:
return false // Impossible
result = false // Impossible
}
if result {
currentWindow.nodeWorkspace?.rootTilingContainer.reveal(currentWindow, preferRightPane: false)
}
return result
}
}

Expand All @@ -52,8 +57,7 @@ struct MoveCommand: Command {
case .stop: return true
case .fail: return false
case .createImplicitContainer:
createImplicitContainerAndMoveWindow(window, workspace, direction)
return true
return createImplicitContainerAndMoveWindow(window, workspace, direction, io)
}
case .allMonitorsOuterFrame:
guard let (monitors, index) = window.nodeMonitor?.findRelativeMonitor(inDirection: direction) else {
Expand All @@ -67,7 +71,7 @@ struct MoveCommand: Command {

return MoveNodeToMonitorCommand(args: moveNodeToMonitorArgs).run(env, io)
} else {
return hitAllMonitorsOuterFrameBoundaries(window, workspace, args, direction)
return hitAllMonitorsOuterFrameBoundaries(window, workspace, args, direction, io)
}
}
}
Expand All @@ -77,13 +81,13 @@ struct MoveCommand: Command {
_ workspace: Workspace,
_ args: MoveCmdArgs,
_ direction: CardinalDirection,
_ io: CmdIo,
) -> Bool {
switch args.boundariesAction {
case .stop: return true
case .fail: return false
case .createImplicitContainer:
createImplicitContainerAndMoveWindow(window, workspace, direction)
return true
return createImplicitContainerAndMoveWindow(window, workspace, direction, io)
}
}

Expand Down Expand Up @@ -125,14 +129,19 @@ private let moveOutMacosUnconventionalWindow = "moving macOS fullscreen, minimiz
_ window: Window,
_ workspace: Workspace,
_ direction: CardinalDirection,
) {
_ io: CmdIo,
) -> Bool {
let prevRoot = workspace.rootTilingContainer
if prevRoot.layout == .scrolling {
return io.err("move --boundaries-action create-implicit-container doesn't support the scrolling layout")
}
prevRoot.unbindFromParent()
// Force tiles layout
_ = TilingContainer(parent: workspace, adaptiveWeight: WEIGHT_AUTO, direction.orientation, .tiles, index: 0)
check(prevRoot != workspace.rootTilingContainer)
prevRoot.bind(to: workspace.rootTilingContainer, adaptiveWeight: WEIGHT_AUTO, index: 0)
window.bind(to: workspace.rootTilingContainer, adaptiveWeight: WEIGHT_AUTO, index: direction.insertionOffset)
return true
}

@MainActor private func deepMoveIn(window: Window, into container: TilingContainer, moveDirection: CardinalDirection) -> Bool {
Expand Down
3 changes: 3 additions & 0 deletions Sources/AppBundle/command/impl/ResizeCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ struct ResizeCommand: Command { // todo cover with tests

func run(_ env: CmdEnv, _ io: CmdIo) -> Bool {
guard let target = args.resolveTargetOrReportError(env, io) else { return false }
if target.workspace.rootTilingContainer.layout == .scrolling {
return io.err("resize command doesn't support the scrolling layout")
}

let candidates = target.windowOrNil?.parentsWithSelf
.filter { ($0.parent as? TilingContainer)?.layout == .tiles }
Expand Down
21 changes: 21 additions & 0 deletions Sources/AppBundle/command/impl/ScrollCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import AppKit
import Common

struct ScrollCommand: Command {
let args: ScrollCmdArgs
/*conforms*/ let shouldResetClosedWindowsCache = false

func run(_ env: CmdEnv, _ io: CmdIo) async throws -> Bool {
let root = focus.workspace.rootTilingContainer
guard root.layout == .scrolling else {
return io.err("scroll command only works when the workspace root container uses the scrolling layout")
}
let direction: CardinalDirection = switch args.direction.val {
case .left: .left
case .right: .right
}
root.clampScrollingIndex()
guard let target = root.scroll(in: direction) else { return true }
return target.focusWindow()
}
}
3 changes: 3 additions & 0 deletions Sources/AppBundle/command/impl/SplitCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ struct SplitCommand: Command {
case .horizontal: .h
case .opposite: parent.orientation.opposite
}
if parent.layout == .scrolling && orientation != .h {
return io.err("The scrolling layout is always horizontal")
}
if parent.children.count == 1 {
parent.changeOrientation(orientation)
} else {
Expand Down
1 change: 1 addition & 0 deletions Sources/AppBundle/focus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ private struct FrozenFocus: AeroAny, Equatable, Sendable {
let status = newFocus.workspace.workspaceMonitor.setActiveWorkspace(newFocus.workspace)

newFocus.windowOrNil?.markAsMostRecentChild()
newFocus.workspace.rootTilingContainer.reveal(newFocus.windowOrNil, preferRightPane: false)
return status
}
extension Window {
Expand Down
6 changes: 6 additions & 0 deletions Sources/AppBundle/initAppBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ import Foundation
private func smartLayoutAtStartup() {
let workspace = focus.workspace
let root = workspace.rootTilingContainer
if config.defaultRootContainerLayout == .scrolling {
root.layout = .scrolling
root.changeOrientation(.h)
root.reveal(focus.windowOrNil, preferRightPane: true)
return
}
if root.children.count <= 3 {
root.layout = .tiles
} else {
Expand Down
31 changes: 31 additions & 0 deletions Sources/AppBundle/layout/layoutRecursive.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ extension TreeNode {
try await container.layoutTiles(point, width: width, height: height, virtual: virtual, context)
case .accordion:
try await container.layoutAccordion(point, width: width, height: height, virtual: virtual, context)
case .scrolling:
try await container.layoutScrolling(point, width: width, height: height, virtual: virtual, context)
}
case .macosMinimizedWindowsContainer, .macosFullscreenWindowsContainer,
.macosPopupWindowsContainer, .macosHiddenAppsWindowsContainer:
Expand Down Expand Up @@ -171,4 +173,33 @@ extension TilingContainer {
}
}
}

@MainActor
fileprivate func layoutScrolling(_ point: CGPoint, width: CGFloat, height: CGFloat, virtual: Rect, _ context: LayoutContext) async throws {
clampScrollingIndex()
switch children.count {
case 0:
return
case 1:
try await children[0].layoutRecursive(point, width: width, height: height, virtual: virtual, context)
default:
let pageWidth = width / 2
let rawGap = context.resolvedGaps.inner.horizontal.toDouble()
for (index, child) in children.enumerated() {
let virtualX = virtual.topLeftX + CGFloat(index) * pageWidth
let physicalX = point.x + CGFloat(index - scrollingIndex) * pageWidth
let isLeftVisiblePage = index == scrollingIndex
let isRightVisiblePage = index == scrollingIndex + 1
let lPadding = isRightVisiblePage ? rawGap / 2 : 0
let rPadding = isLeftVisiblePage ? rawGap / 2 : 0
try await child.layoutRecursive(
CGPoint(x: physicalX + lPadding, y: point.y),
width: pageWidth - lPadding - rPadding,
height: height,
virtual: Rect(topLeftX: virtualX, topLeftY: virtual.topLeftY, width: pageWidth, height: height),
context,
)
}
}
}
}
4 changes: 4 additions & 0 deletions Sources/AppBundle/mouse/moveWithMouse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ extension CGPoint {
})
case .accordion:
tree.mostRecentChild
case .scrolling:
tree.children.first(where: {
(virtual ? $0.lastAppliedLayoutVirtualRect : $0.lastAppliedLayoutPhysicalRect)?.contains(point) == true
})
}
guard let target else { return nil }
return switch target.tilingTreeNodeCasesOrDie() {
Expand Down
55 changes: 55 additions & 0 deletions Sources/AppBundle/tree/TilingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ final class TilingContainer: TreeNode, NonLeafTreeNodeObject { // todo consider
fileprivate var _orientation: Orientation
var orientation: Orientation { _orientation }
var layout: Layout
private var _scrollingIndex: Int = 0
var scrollingIndex: Int {
get { _scrollingIndex }
set { _scrollingIndex = max(0, newValue) }
}

@MainActor
init(parent: NonLeafTreeNodeObject, adaptiveWeight: CGFloat, _ orientation: Orientation, _ layout: Layout, index: Int) {
Expand All @@ -26,6 +31,55 @@ final class TilingContainer: TreeNode, NonLeafTreeNodeObject { // todo consider

extension TilingContainer {
var isRootContainer: Bool { parent is Workspace }
var isScrollingRoot: Bool { isRootContainer && layout == .scrolling }

var maxScrollingIndex: Int { max(0, children.count - 2) }

func clampScrollingIndex() {
scrollingIndex = isScrollingRoot ? min(scrollingIndex, maxScrollingIndex) : 0
}

@MainActor
func reveal(_ window: Window?, preferRightPane: Bool) {
guard isScrollingRoot else { return }
guard let index = window.flatMap(scrollingPageIndex(for:)) else {
clampScrollingIndex()
return
}
scrollingIndex = preferRightPane
? min(max(0, index - 1), maxScrollingIndex)
: min(
max(
0,
index < scrollingIndex
? index
: (index > scrollingIndex + 1 ? index - 1 : scrollingIndex),
),
maxScrollingIndex,
)
}

@MainActor
func scroll(in direction: CardinalDirection) -> Window? {
guard isScrollingRoot else { return nil }
switch direction {
case .left:
let nextIndex = max(0, scrollingIndex - 1)
scrollingIndex = nextIndex
return children.getOrNil(atIndex: nextIndex)?.findLeafWindowRecursive(snappedTo: .left)
case .right:
let nextIndex = min(maxScrollingIndex, scrollingIndex + 1)
scrollingIndex = nextIndex
return children.getOrNil(atIndex: nextIndex + 1)?.findLeafWindowRecursive(snappedTo: .right)
case .up, .down:
return nil
}
}

@MainActor
private func scrollingPageIndex(for window: Window) -> Int? {
window.parentsWithSelf.first(where: { $0.parent === self })?.ownIndex
}

@MainActor
func changeOrientation(_ targetOrientation: Orientation) {
Expand Down Expand Up @@ -58,6 +112,7 @@ extension TilingContainer {
enum Layout: String {
case tiles
case accordion
case scrolling
}

extension String {
Expand Down
Loading
Loading