Skip to content

Commit 4f5f7db

Browse files
liangyuetianIgor SpivakclaudeIgor Spivakrxhanson
committed
Configurable split ratio for half window actions (rxhanson#1712)
* feat: Configurable split ratio for half window actions Left/right and top/bottom half actions now support a custom split ratio instead of the fixed 50/50. Configure via Extra Settings (⋯) in the General preferences tab, or via terminal with horizontalSplitRatio and verticalSplitRatio defaults (1–99, default 50). The drag-to-snap footprint preview also reflects the configured ratio. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: Align half split ratio UI with existing extra settings layout Center the section header and pin the input fields' trailing edges to match the shortcut views, consistent with how Width Step is laid out. Remove the half split ratio section from TerminalCommands.md since it is now configurable in the GUI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Remove CLAUDE.md from git tracking * Fix missing comma from merge conflict resolution --------- Co-authored-by: Igor Spivak <ispivak@terminal-grace.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Igor Spivak <ispivak@terminal-grace.localdomain> Co-authored-by: Ryan Hanson <ryan@ryanhanson.dev>
1 parent f7cc415 commit 4f5f7db

6 files changed

Lines changed: 97 additions & 13 deletions

File tree

Rectangle/Defaults.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ class Defaults {
6767
static let notifiedOfProblemApps = BoolDefault(key: "notifiedOfProblemApps")
6868
static let specifiedHeight = FloatDefault(key: "specifiedHeight", defaultValue: 1050)
6969
static let specifiedWidth = FloatDefault(key: "specifiedWidth", defaultValue: 1680)
70+
static let horizontalSplitRatio = FloatDefault(key: "horizontalSplitRatio", defaultValue: 50)
71+
static let verticalSplitRatio = FloatDefault(key: "verticalSplitRatio", defaultValue: 50)
7072
static let moveCursorAcrossDisplays = OptionalBoolDefault(key: "moveCursorAcrossDisplays")
7173
static let moveCursor = OptionalBoolDefault(key: "moveCursor")
7274
static let autoMaximize = OptionalBoolDefault(key: "autoMaximize")
@@ -151,6 +153,8 @@ class Defaults {
151153
notifiedOfProblemApps,
152154
specifiedHeight,
153155
specifiedWidth,
156+
horizontalSplitRatio,
157+
verticalSplitRatio,
154158
moveCursorAcrossDisplays,
155159
moveCursor,
156160
autoMaximize,

Rectangle/PrefsWindow/SettingsViewController.swift

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,44 @@ class SettingsViewController: NSViewController {
350350
integerFormatter.minimum = 1
351351
widthStepField.formatter = integerFormatter
352352

353+
let splitRatioHeaderLabel = NSTextField(labelWithString: NSLocalizedString("Half Split Ratios", tableName: "Main", value: "", comment: ""))
354+
splitRatioHeaderLabel.font = NSFont.boldSystemFont(ofSize: NSFont.systemFontSize)
355+
splitRatioHeaderLabel.alignment = .center
356+
splitRatioHeaderLabel.translatesAutoresizingMaskIntoConstraints = false
357+
358+
let hSplitLabel = NSTextField(labelWithString: NSLocalizedString("Horizontal (L/R, %)", tableName: "Main", value: "", comment: ""))
359+
hSplitLabel.alignment = .right
360+
hSplitLabel.translatesAutoresizingMaskIntoConstraints = false
361+
362+
let vSplitLabel = NSTextField(labelWithString: NSLocalizedString("Vertical (T/B, %)", tableName: "Main", value: "", comment: ""))
363+
vSplitLabel.alignment = .right
364+
vSplitLabel.translatesAutoresizingMaskIntoConstraints = false
365+
366+
let percentFormatter = NumberFormatter()
367+
percentFormatter.allowsFloats = false
368+
percentFormatter.minimum = 1
369+
percentFormatter.maximum = 99
370+
371+
let hSplitField = AutoSaveFloatField(frame: NSRect(x: 0, y: 0, width: 160, height: 19))
372+
hSplitField.stringValue = String(Int(Defaults.horizontalSplitRatio.value))
373+
hSplitField.delegate = self
374+
hSplitField.defaults = Defaults.horizontalSplitRatio
375+
hSplitField.fallbackValue = 50
376+
hSplitField.translatesAutoresizingMaskIntoConstraints = false
377+
hSplitField.refusesFirstResponder = true
378+
hSplitField.alignment = .right
379+
hSplitField.formatter = percentFormatter
380+
381+
let vSplitField = AutoSaveFloatField(frame: NSRect(x: 0, y: 0, width: 160, height: 19))
382+
vSplitField.stringValue = String(Int(Defaults.verticalSplitRatio.value))
383+
vSplitField.delegate = self
384+
vSplitField.defaults = Defaults.verticalSplitRatio
385+
vSplitField.fallbackValue = 50
386+
vSplitField.translatesAutoresizingMaskIntoConstraints = false
387+
vSplitField.refusesFirstResponder = true
388+
vSplitField.alignment = .right
389+
vSplitField.formatter = percentFormatter
390+
353391
largerWidthShortcutView.setAssociatedUserDefaultsKey(WindowAction.largerWidth.name, withTransformerName: MASDictionaryTransformerName)
354392
smallerWidthShortcutView.setAssociatedUserDefaultsKey(WindowAction.smallerWidth.name, withTransformerName: MASDictionaryTransformerName)
355393

@@ -572,6 +610,20 @@ class SettingsViewController: NSViewController {
572610
widthStepRow.spacing = 18
573611
widthStepRow.addArrangedSubview(widthStepLabel)
574612
widthStepRow.addArrangedSubview(widthStepField)
613+
614+
let hSplitRow = NSStackView()
615+
hSplitRow.orientation = .horizontal
616+
hSplitRow.alignment = .centerY
617+
hSplitRow.spacing = 18
618+
hSplitRow.addArrangedSubview(hSplitLabel)
619+
hSplitRow.addArrangedSubview(hSplitField)
620+
621+
let vSplitRow = NSStackView()
622+
vSplitRow.orientation = .horizontal
623+
vSplitRow.alignment = .centerY
624+
vSplitRow.spacing = 18
625+
vSplitRow.addArrangedSubview(vSplitLabel)
626+
vSplitRow.addArrangedSubview(vSplitField)
575627

576628
let topVerticalThirdRow = NSStackView()
577629
topVerticalThirdRow.orientation = .horizontal
@@ -682,9 +734,14 @@ class SettingsViewController: NSViewController {
682734
mainStackView.addArrangedSubview(bottomCenterLeftEighthRow)
683735
mainStackView.addArrangedSubview(bottomCenterRightEighthRow)
684736
mainStackView.addArrangedSubview(bottomRightEighthRow)
737+
mainStackView.addArrangedSubview(splitRatioHeaderLabel)
738+
mainStackView.setCustomSpacing(10, after: splitRatioHeaderLabel)
739+
mainStackView.addArrangedSubview(hSplitRow)
740+
mainStackView.addArrangedSubview(vSplitRow)
685741

686742
NSLayoutConstraint.activate([
687743
headerLabel.widthAnchor.constraint(equalTo: mainStackView.widthAnchor),
744+
splitRatioHeaderLabel.widthAnchor.constraint(equalTo: mainStackView.widthAnchor),
688745
largerWidthLabel.widthAnchor.constraint(equalTo: smallerWidthLabel.widthAnchor),
689746
smallerWidthLabel.widthAnchor.constraint(equalTo: widthStepLabel.widthAnchor),
690747
widthStepLabel.widthAnchor.constraint(equalTo: topVerticalThirdLabel.widthAnchor),
@@ -700,6 +757,8 @@ class SettingsViewController: NSViewController {
700757
bottomLeftEighthLabel.widthAnchor.constraint(equalTo: bottomCenterLeftEighthLabel.widthAnchor),
701758
bottomCenterLeftEighthLabel.widthAnchor.constraint(equalTo: bottomCenterRightEighthLabel.widthAnchor),
702759
bottomCenterRightEighthLabel.widthAnchor.constraint(equalTo: bottomRightEighthLabel.widthAnchor),
760+
bottomVerticalTwoThirdsLabel.widthAnchor.constraint(equalTo: hSplitLabel.widthAnchor),
761+
hSplitLabel.widthAnchor.constraint(equalTo: vSplitLabel.widthAnchor),
703762
largerWidthLabelStack.widthAnchor.constraint(equalTo: smallerWidthLabelStack.widthAnchor),
704763
largerWidthShortcutView.widthAnchor.constraint(equalToConstant: 160),
705764
smallerWidthShortcutView.widthAnchor.constraint(equalToConstant: 160),
@@ -717,7 +776,12 @@ class SettingsViewController: NSViewController {
717776
bottomCenterLeftEighthShortcutView.widthAnchor.constraint(equalToConstant: 160),
718777
bottomCenterRightEighthShortcutView.widthAnchor.constraint(equalToConstant: 160),
719778
bottomRightEighthShortcutView.widthAnchor.constraint(equalToConstant: 160),
720-
widthStepField.trailingAnchor.constraint(equalTo: largerWidthShortcutView.trailingAnchor)
779+
widthStepField.trailingAnchor.constraint(equalTo: largerWidthShortcutView.trailingAnchor),
780+
hSplitField.widthAnchor.constraint(equalToConstant: 160),
781+
vSplitField.widthAnchor.constraint(equalToConstant: 160),
782+
widthStepField.trailingAnchor.constraint(equalTo: largerWidthShortcutView.trailingAnchor),
783+
hSplitField.trailingAnchor.constraint(equalTo: largerWidthShortcutView.trailingAnchor),
784+
vSplitField.trailingAnchor.constraint(equalTo: largerWidthShortcutView.trailingAnchor)
721785
])
722786

723787
let containerView = NSView()
@@ -966,8 +1030,9 @@ extension SettingsViewController: NSTextFieldDelegate {
9661030
let defaults: FloatDefault = sender.defaults else { return }
9671031

9681032
if sender.stringValue.isEmpty {
969-
sender.stringValue = "30"
970-
defaults.value = 30
1033+
let fallback = sender.fallbackValue
1034+
sender.stringValue = "\(Int(fallback))"
1035+
defaults.value = fallback
9711036
sender.defaultsSetAction?()
9721037
}
9731038
}
@@ -976,4 +1041,5 @@ extension SettingsViewController: NSTextFieldDelegate {
9761041
class AutoSaveFloatField: NSTextField {
9771042
var defaults: FloatDefault?
9781043
var defaultsSetAction: (() -> Void)?
1044+
var fallbackValue: Float = 30
9791045
}

Rectangle/WindowCalculation/BottomHalfCalculation.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class BottomHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCalcul
1919
return calculateRepeatedRect(params)
2020
}
2121

22+
func calculateFirstRect(_ params: RectCalculationParameters) -> RectResult {
23+
return calculateFractionalRect(params, fraction: 1.0 - Defaults.verticalSplitRatio.value / 100.0)
24+
}
25+
2226
func calculateFractionalRect(_ params: RectCalculationParameters, fraction: Float) -> RectResult {
2327
let visibleFrameOfScreen = params.visibleFrameOfScreen
2428

Rectangle/WindowCalculation/LeftRightHalfCalculation.swift

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,22 @@ class LeftRightHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCal
3333

3434
}
3535

36+
func calculateFirstRect(_ params: RectCalculationParameters) -> RectResult {
37+
let ratio = Defaults.horizontalSplitRatio.value / 100.0
38+
let fraction = params.action == .rightHalf ? 1.0 - ratio : ratio
39+
return calculateFractionalRect(params, fraction: fraction)
40+
}
41+
3642
func calculateFractionalRect(_ params: RectCalculationParameters, fraction: Float) -> RectResult {
3743
let visibleFrameOfScreen = params.visibleFrameOfScreen
3844

3945
var rect = visibleFrameOfScreen
40-
46+
4147
rect.size.width = floor(visibleFrameOfScreen.width * CGFloat(fraction))
4248
if params.action == .rightHalf {
4349
rect.origin.x = visibleFrameOfScreen.maxX - rect.width
4450
}
45-
51+
4652
return RectResult(rect)
4753
}
4854

@@ -96,15 +102,15 @@ class LeftRightHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCal
96102

97103
// Used to draw box for snapping
98104
override func calculateRect(_ params: RectCalculationParameters) -> RectResult {
105+
let ratio = CGFloat(Defaults.horizontalSplitRatio.value / 100.0)
106+
var rect = params.visibleFrameOfScreen
107+
let leftWidth = floor(rect.width * ratio)
99108
if params.action == .leftHalf {
100-
var oneHalfRect = params.visibleFrameOfScreen
101-
oneHalfRect.size.width = floor(oneHalfRect.width / 2.0)
102-
return RectResult(oneHalfRect)
109+
rect.size.width = leftWidth
103110
} else {
104-
var oneHalfRect = params.visibleFrameOfScreen
105-
oneHalfRect.size.width = floor(oneHalfRect.width / 2.0)
106-
oneHalfRect.origin.x += oneHalfRect.size.width
107-
return RectResult(oneHalfRect)
111+
rect.size.width = rect.width - leftWidth
112+
rect.origin.x += leftWidth
108113
}
114+
return RectResult(rect)
109115
}
110116
}

Rectangle/WindowCalculation/TopHalfCalculation.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class TopHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCalculati
1919
return calculateRepeatedRect(params)
2020
}
2121

22+
func calculateFirstRect(_ params: RectCalculationParameters) -> RectResult {
23+
return calculateFractionalRect(params, fraction: Defaults.verticalSplitRatio.value / 100.0)
24+
}
25+
2226
func calculateFractionalRect(_ params: RectCalculationParameters, fraction: Float) -> RectResult {
2327
let visibleFrameOfScreen = params.visibleFrameOfScreen
2428

TerminalCommands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,4 +516,4 @@ By default, display order is left-to-right, line-by-line. You can change this to
516516

517517
```bash
518518
defaults write com.knollsoft.Rectangle screensOrderedByX -int 1
519-
```
519+
```

0 commit comments

Comments
 (0)