Merge pull request #822 from gpanders/split-resizing

macos: implement split resizing
This commit is contained in:
Mitchell Hashimoto
2023-11-06 09:35:43 -08:00
committed by GitHub
16 changed files with 466 additions and 159 deletions

View File

@@ -38,6 +38,11 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
@IBOutlet private var menuResetFontSize: NSMenuItem?
@IBOutlet private var menuTerminalInspector: NSMenuItem?
@IBOutlet private var menuMoveSplitDividerUp: NSMenuItem?
@IBOutlet private var menuMoveSplitDividerDown: NSMenuItem?
@IBOutlet private var menuMoveSplitDividerLeft: NSMenuItem?
@IBOutlet private var menuMoveSplitDividerRight: NSMenuItem?
/// The dock menu
private var dockMenu: NSMenu = NSMenu()
@@ -206,6 +211,10 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
syncMenuShortcut(action: "goto_split:bottom", menuItem: self.menuSelectSplitBelow)
syncMenuShortcut(action: "goto_split:left", menuItem: self.menuSelectSplitLeft)
syncMenuShortcut(action: "goto_split:right", menuItem: self.menuSelectSplitRight)
syncMenuShortcut(action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp)
syncMenuShortcut(action: "resize_split:down,10", menuItem: self.menuMoveSplitDividerDown)
syncMenuShortcut(action: "resize_split:right,10", menuItem: self.menuMoveSplitDividerRight)
syncMenuShortcut(action: "resize_split:left,10", menuItem: self.menuMoveSplitDividerLeft)
syncMenuShortcut(action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)

View File

@@ -40,7 +40,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
// Initialize our initial surface.
guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") }
self.surfaceTree = .noSplit(.init(ghostty_app, base))
self.surfaceTree = .leaf(.init(ghostty_app, base))
// Setup our notifications for behaviors
let center = NotificationCenter.default
@@ -270,7 +270,27 @@ class TerminalController: NSWindowController, NSWindowDelegate,
@IBAction func splitMoveFocusRight(_ sender: Any) {
splitMoveFocus(direction: .right)
}
@IBAction func moveSplitDividerUp(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return }
ghostty.splitResize(surface: surface, direction: .up, amount: 10)
}
@IBAction func moveSplitDividerDown(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return }
ghostty.splitResize(surface: surface, direction: .down, amount: 10)
}
@IBAction func moveSplitDividerLeft(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return }
ghostty.splitResize(surface: surface, direction: .left, amount: 10)
}
@IBAction func moveSplitDividerRight(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return }
ghostty.splitResize(surface: surface, direction: .right, amount: 10)
}
private func splitMoveFocus(direction: Ghostty.SplitFocusDirection) {
guard let surface = focusedSurface?.surface else { return }
ghostty.splitMoveFocus(surface: surface, direction: direction)

View File

@@ -84,8 +84,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
}
Ghostty.TerminalSplit(node: $viewModel.surfaceTree)
.ghosttyApp(ghostty.app!)
.ghosttyConfig(ghostty.config!)
.environmentObject(ghostty)
.focused($focused)
.onAppear { self.focused = true }
.onChange(of: focusedSurface) { newValue in

View File

@@ -158,6 +158,8 @@ extension Ghostty {
control_inspector_cb: { userdata, mode in AppState.controlInspector(userdata, mode: mode) },
close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) },
focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) },
resize_split_cb: { userdata, direction, amount in
AppState.resizeSplit(userdata, direction: direction, amount: amount) },
toggle_split_zoom_cb: { userdata in AppState.toggleSplitZoom(userdata) },
goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) },
toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) },
@@ -292,6 +294,10 @@ extension Ghostty {
ghostty_surface_split_focus(surface, direction.toNative())
}
func splitResize(surface: ghostty_surface_t, direction: SplitResizeDirection, amount: UInt16) {
ghostty_surface_split_resize(surface, direction.toNative(), amount)
}
func splitToggleZoom(surface: ghostty_surface_t) {
let action = "toggle_split_zoom"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
@@ -364,6 +370,19 @@ extension Ghostty {
)
}
static func resizeSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_resize_direction_e, amount: UInt16) {
guard let surface = self.surfaceUserdata(from: userdata) else { return }
guard let resizeDirection = SplitResizeDirection.from(direction: direction) else { return }
NotificationCenter.default.post(
name: Notification.didResizeSplit,
object: surface,
userInfo: [
Notification.ResizeSplitDirectionKey: resizeDirection,
Notification.ResizeSplitAmountKey: amount,
]
)
}
static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) {
guard let surface = self.surfaceUserdata(from: userdata) else { return }
@@ -572,35 +591,3 @@ extension Ghostty {
}
}
}
// MARK: AppState Environment Keys
private struct GhosttyAppKey: EnvironmentKey {
static let defaultValue: ghostty_app_t? = nil
}
private struct GhosttyConfigKey: EnvironmentKey {
static let defaultValue: ghostty_config_t? = nil
}
extension EnvironmentValues {
var ghosttyApp: ghostty_app_t? {
get { self[GhosttyAppKey.self] }
set { self[GhosttyAppKey.self] = newValue }
}
var ghosttyConfig: ghostty_config_t? {
get { self[GhosttyConfigKey.self] }
set { self[GhosttyConfigKey.self] = newValue }
}
}
extension View {
func ghosttyApp(_ app: ghostty_app_t?) -> some View {
environment(\.ghosttyApp, app)
}
func ghosttyConfig(_ config: ghostty_config_t?) -> some View {
environment(\.ghosttyConfig, config)
}
}

View File

@@ -1,4 +1,5 @@
import SwiftUI
import Combine
import GhosttyKit
extension Ghostty {
@@ -11,9 +12,8 @@ extension Ghostty {
/// values can further be split infinitely.
///
enum SplitNode: Equatable, Hashable {
case noSplit(Leaf)
case horizontal(Container)
case vertical(Container)
case leaf(Leaf)
case split(Container)
/// Returns the view that would prefer receiving focus in this tree. This is always the
/// top-left-most view. This is used when creating a split or closing a split to find the
@@ -21,14 +21,11 @@ extension Ghostty {
func preferredFocus(_ direction: SplitFocusDirection = .top) -> SurfaceView {
let container: Container
switch (self) {
case .noSplit(let leaf):
case .leaf(let leaf):
// noSplit is easy because there is only one thing to focus
return leaf.surface
case .horizontal(let c):
container = c
case .vertical(let c):
case .split(let c):
container = c
}
@@ -48,14 +45,10 @@ extension Ghostty {
/// surface. At this point, the surface view in this node tree can never be used again.
func close() {
switch (self) {
case .noSplit(let leaf):
case .leaf(let leaf):
leaf.surface.close()
case .horizontal(let container):
container.topLeft.close()
container.bottomRight.close()
case .vertical(let container):
case .split(let container):
container.topLeft.close()
container.bottomRight.close()
}
@@ -64,14 +57,10 @@ extension Ghostty {
/// Returns true if any surface in the split stack requires quit confirmation.
func needsConfirmQuit() -> Bool {
switch (self) {
case .noSplit(let leaf):
case .leaf(let leaf):
return leaf.surface.needsConfirmQuit
case .horizontal(let container):
return container.topLeft.needsConfirmQuit() ||
container.bottomRight.needsConfirmQuit()
case .vertical(let container):
case .split(let container):
return container.topLeft.needsConfirmQuit() ||
container.bottomRight.needsConfirmQuit()
}
@@ -80,14 +69,10 @@ extension Ghostty {
/// Returns true if the split tree contains the given view.
func contains(view: SurfaceView) -> Bool {
switch (self) {
case .noSplit(let leaf):
case .leaf(let leaf):
return leaf.surface == view
case .horizontal(let container):
return container.topLeft.contains(view: view) ||
container.bottomRight.contains(view: view)
case .vertical(let container):
case .split(let container):
return container.topLeft.contains(view: view) ||
container.bottomRight.contains(view: view)
}
@@ -97,11 +82,9 @@ extension Ghostty {
static func == (lhs: SplitNode, rhs: SplitNode) -> Bool {
switch (lhs, rhs) {
case (.noSplit(let lhs_v), .noSplit(let rhs_v)):
case (.leaf(let lhs_v), .leaf(let rhs_v)):
return lhs_v === rhs_v
case (.horizontal(let lhs_v), .horizontal(let rhs_v)):
return lhs_v === rhs_v
case (.vertical(let lhs_v), .vertical(let rhs_v)):
case (.split(let lhs_v), .split(let rhs_v)):
return lhs_v === rhs_v
default:
return false
@@ -112,6 +95,8 @@ extension Ghostty {
let app: ghostty_app_t
@Published var surface: SurfaceView
weak var parent: SplitNode.Container?
/// Initialize a new leaf which creates a new terminal surface.
init(_ app: ghostty_app_t, _ baseConfig: SurfaceConfiguration?) {
self.app = app
@@ -134,25 +119,63 @@ extension Ghostty {
class Container: ObservableObject, Equatable, Hashable {
let app: ghostty_app_t
let direction: SplitViewDirection
@Published var topLeft: SplitNode
@Published var bottomRight: SplitNode
var resizeEvent: PassthroughSubject<Double, Never> = .init()
weak var parent: SplitNode.Container?
/// A container is always initialized from some prior leaf because a split has to originate
/// from a non-split value. When initializing, we inherit the leaf's surface and then
/// initialize a new surface for the new pane.
init(from: Leaf, baseConfig: SurfaceConfiguration? = nil) {
init(from: Leaf, direction: SplitViewDirection, baseConfig: SurfaceConfiguration? = nil) {
self.app = from.app
self.direction = direction
self.parent = from.parent
// Initially, both topLeft and bottomRight are in the "nosplit"
// state since this is a new split.
self.topLeft = .noSplit(from)
self.bottomRight = .noSplit(.init(app, baseConfig))
self.topLeft = .leaf(from)
let bottomRight: Leaf = .init(app, baseConfig)
self.bottomRight = .leaf(bottomRight)
from.parent = self
bottomRight.parent = self
}
/// Resize the split by moving the split divider in the given
/// direction by the given amount. If this container is not split
/// in the given direction, navigate up the tree until we find a
/// container that is
func resize(direction: SplitResizeDirection, amount: UInt16) {
// We send a resize event to our publisher which will be
// received by the SplitView.
switch (self.direction) {
case .horizontal:
switch (direction) {
case .left: resizeEvent.send(-Double(amount))
case .right: resizeEvent.send(Double(amount))
default: parent?.resize(direction: direction, amount: amount)
}
case .vertical:
switch (direction) {
case .up: resizeEvent.send(-Double(amount))
case .down: resizeEvent.send(Double(amount))
default: parent?.resize(direction: direction, amount: amount)
}
}
}
// MARK: - Hashable
func hash(into hasher: inout Hasher) {
hasher.combine(app)
hasher.combine(direction)
hasher.combine(topLeft)
hasher.combine(bottomRight)
}
@@ -161,6 +184,7 @@ extension Ghostty {
static func == (lhs: Container, rhs: Container) -> Bool {
return lhs.app == rhs.app &&
lhs.direction == rhs.direction &&
lhs.topLeft == rhs.topLeft &&
lhs.bottomRight == rhs.bottomRight
}

View File

@@ -33,7 +33,7 @@ extension Ghostty {
.focusedValue(\.ghosttySurfaceZoomed, zoomedSurface != nil)
}
}
/// The root of a split tree. This sets up the initial SplitNode state and renders. There is only ever
/// one of these in a split tree.
private struct TerminalSplitRoot: View {
@@ -61,25 +61,15 @@ extension Ghostty {
case nil:
Color(.clear)
case .noSplit(let leaf):
case .leaf(let leaf):
TerminalSplitLeaf(
leaf: leaf,
neighbors: .empty,
node: $node
)
case .horizontal(let container):
case .split(let container):
TerminalSplitContainer(
direction: .horizontal,
neighbors: .empty,
node: $node,
container: container
)
.onReceive(pubZoom) { onZoom(notification: $0) }
case .vertical(let container):
TerminalSplitContainer(
direction: .vertical,
neighbors: .empty,
node: $node,
container: container
@@ -105,7 +95,7 @@ extension Ghostty {
func onZoom(notification: SwiftUI.Notification) {
// Our node must be split to receive zooms. You can't zoom an unsplit terminal.
if case .noSplit = node {
if case .leaf = node {
preconditionFailure("TerminalSplitRoom must not be zoom-able if no splits exist")
}
@@ -163,11 +153,13 @@ extension Ghostty {
let pub = center.publisher(for: Notification.ghosttyNewSplit, object: leaf.surface)
let pubClose = center.publisher(for: Notification.ghosttyCloseSurface, object: leaf.surface)
let pubFocus = center.publisher(for: Notification.ghosttyFocusSplit, object: leaf.surface)
let pubResize = center.publisher(for: Notification.didResizeSplit, object: leaf.surface)
InspectableSurface(surfaceView: leaf.surface, isSplit: !neighbors.isEmpty())
.onReceive(pub) { onNewSplit(notification: $0) }
.onReceive(pubClose) { onClose(notification: $0) }
.onReceive(pubFocus) { onMoveFocus(notification: $0) }
.onReceive(pubResize) { onResize(notification: $0) }
}
private func onClose(notification: SwiftUI.Notification) {
@@ -234,16 +226,10 @@ extension Ghostty {
}
// Setup our new container since we are now split
let container = SplitNode.Container(from: leaf, baseConfig: config)
let container = SplitNode.Container(from: leaf, direction: splitDirection, baseConfig: config)
// Depending on the direction, change the parent node. This will trigger
// the parent to relayout our views.
switch (splitDirection) {
case .horizontal:
node = .horizontal(container)
case .vertical:
node = .vertical(container)
}
// Change the parent node. This will trigger the parent to relayout our views.
node = .split(container)
// See moveFocus comment, we have to run this whenever split changes.
Ghostty.moveFocus(to: container.bottomRight.preferredFocus(), from: node!.preferredFocus())
@@ -259,18 +245,35 @@ extension Ghostty {
to: next.preferredFocus(direction)
)
}
/// Handle a resize event.
private func onResize(notification: SwiftUI.Notification) {
// If this leaf is not part of a split then there is nothing to do
guard let parent = leaf.parent else { return }
guard let directionAny = notification.userInfo?[Ghostty.Notification.ResizeSplitDirectionKey] else { return }
guard let direction = directionAny as? Ghostty.SplitResizeDirection else { return }
guard let amountAny = notification.userInfo?[Ghostty.Notification.ResizeSplitAmountKey] else { return }
guard let amount = amountAny as? UInt16 else { return }
parent.resize(direction: direction, amount: amount)
}
}
/// This represents a split view that is in the horizontal or vertical split state.
private struct TerminalSplitContainer: View {
let direction: SplitViewDirection
let neighbors: SplitNode.Neighbors
@Binding var node: SplitNode?
@StateObject var container: SplitNode.Container
var body: some View {
SplitView(direction, left: {
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = direction == .horizontal ? \.right : \.bottom
SplitView(
container.direction,
resizeIncrements: .init(width: 1, height: 1),
resizePublisher: container.resizeEvent,
left: {
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = container.direction == .horizontal ? \.right : \.bottom
TerminalSplitNested(
node: closeableTopLeft(),
@@ -280,8 +283,8 @@ extension Ghostty {
])
)
}, right: {
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = direction == .horizontal ? \.left : \.top
let neighborKey: WritableKeyPath<SplitNode.Neighbors, SplitNode?> = container.direction == .horizontal ? \.left : \.top
TerminalSplitNested(
node: closeableBottomRight(),
neighbors: neighbors.update([
@@ -304,7 +307,16 @@ extension Ghostty {
// Closing
container.topLeft.close()
node = container.bottomRight
switch (node) {
case .leaf(let l):
l.parent = container.parent
case .split(let c):
c.parent = container.parent
case .none:
break
}
DispatchQueue.main.async {
Ghostty.moveFocus(
to: container.bottomRight.preferredFocus(),
@@ -326,7 +338,16 @@ extension Ghostty {
// Closing
container.bottomRight.close()
node = container.topLeft
switch (node) {
case .leaf(let l):
l.parent = container.parent
case .split(let c):
c.parent = container.parent
case .none:
break
}
DispatchQueue.main.async {
Ghostty.moveFocus(
to: container.topLeft.preferredFocus(),
@@ -350,24 +371,15 @@ extension Ghostty {
case nil:
Color(.clear)
case .noSplit(let leaf):
case .leaf(let leaf):
TerminalSplitLeaf(
leaf: leaf,
neighbors: neighbors,
node: $node
)
case .horizontal(let container):
case .split(let container):
TerminalSplitContainer(
direction: .horizontal,
neighbors: neighbors,
node: $node,
container: container
)
case .vertical(let container):
TerminalSplitContainer(
direction: .vertical,
neighbors: neighbors,
node: $node,
container: container

View File

@@ -61,6 +61,39 @@ extension Ghostty {
}
}
}
/// Enum used for resizing splits. This is the direction the split divider will move.
enum SplitResizeDirection {
case up, down, left, right
static func from(direction: ghostty_split_resize_direction_e) -> Self? {
switch (direction) {
case GHOSTTY_SPLIT_RESIZE_UP:
return .up;
case GHOSTTY_SPLIT_RESIZE_DOWN:
return .down;
case GHOSTTY_SPLIT_RESIZE_LEFT:
return .left;
case GHOSTTY_SPLIT_RESIZE_RIGHT:
return .right;
default:
return nil
}
}
func toNative() -> ghostty_split_resize_direction_e {
switch (self) {
case .up:
return GHOSTTY_SPLIT_RESIZE_UP;
case .down:
return GHOSTTY_SPLIT_RESIZE_DOWN;
case .left:
return GHOSTTY_SPLIT_RESIZE_LEFT;
case .right:
return GHOSTTY_SPLIT_RESIZE_RIGHT;
}
}
}
}
extension Ghostty.Notification {
@@ -112,6 +145,11 @@ extension Ghostty.Notification {
static let confirmUnsafePaste = Notification.Name("com.mitchellh.ghostty.confirmUnsafePaste")
static let UnsafePasteStrKey = confirmUnsafePaste.rawValue + ".str"
static let UnsafePasteStateKey = confirmUnsafePaste.rawValue + ".state"
/// Notification sent to the active split view to resize the split.
static let didResizeSplit = Notification.Name("com.mitchellh.ghostty.didResizeSplit")
static let ResizeSplitDirectionKey = didResizeSplit.rawValue + ".direction"
static let ResizeSplitAmountKey = didResizeSplit.rawValue + ".amount"
}
// Make the input enum hashable.

View File

@@ -4,11 +4,11 @@ import GhosttyKit
extension Ghostty {
/// Render a terminal for the active app in the environment.
struct Terminal: View {
@Environment(\.ghosttyApp) private var app
@EnvironmentObject private var ghostty: Ghostty.AppState
@FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String?
var body: some View {
if let app = self.app {
if let app = self.ghostty.app {
SurfaceForApp(app) { surfaceView in
SurfaceWrapper(surfaceView: surfaceView)
}
@@ -48,7 +48,7 @@ extension Ghostty {
// Maintain whether our window has focus (is key) or not
@State private var windowFocus: Bool = true
@Environment(\.ghosttyConfig) private var ghostty_config
@EnvironmentObject private var ghostty: Ghostty.AppState
// This is true if the terminal is considered "focused". The terminal is focused if
// it is both individually focused and the containing window is key.
@@ -58,7 +58,7 @@ extension Ghostty {
private var unfocusedOpacity: Double {
var opacity: Double = 0.85
let key = "unfocused-split-opacity"
_ = ghostty_config_get(ghostty_config, &opacity, key, UInt(key.count))
_ = ghostty_config_get(ghostty.config, &opacity, key, UInt(key.count))
return 1 - opacity
}
@@ -508,7 +508,7 @@ extension Ghostty {
guard let window = self.window else { return }
guard let windowControllerRaw = window.windowController else { return }
guard let windowController = windowControllerRaw as? TerminalController else { return }
guard case .noSplit = windowController.surfaceTree else { return }
guard case .leaf = windowController.surfaceTree else { return }
// If our window is full screen, we do not set the frame
guard !window.styleMask.contains(.fullScreen) else { return }

View File

@@ -1,4 +1,5 @@
import SwiftUI
import Combine
/// A split view shows a left and right (or top and bottom) view with a divider in the middle to do resizing.
/// The terminlogy "left" and "right" is always used but for vertical splits "left" is "top" and "right" is "bottom".
@@ -9,13 +10,20 @@ struct SplitView<L: View, R: View>: View {
/// Direction of the split
let direction: SplitViewDirection
/// If set, the split view supports programmatic resizing via events sent via the publisher.
/// Minimum increment (in points) that this split can be resized by, in
/// each direction. Both `height` and `width` should be whole numbers
/// greater than or equal to 1.0
let resizeIncrements: NSSize
let resizePublisher: PassthroughSubject<Double, Never>
/// The left and right views to render.
let left: L
let right: R
/// The current fractional width of the split view. 0.5 means L/R are equally sized, for example.
@State var split: CGFloat = 0.5
/// The visible size of the splitter, in points. The invisible size is a transparent hitbox that can still
/// be used for getting a resize handle. The total width/height of the splitter is the sum of both.
private let splitterVisibleSize: CGFloat = 1
@@ -38,15 +46,51 @@ struct SplitView<L: View, R: View>: View {
.position(splitterPoint)
.gesture(dragGesture(geo.size, splitterPoint: splitterPoint))
}
.onReceive(resizePublisher) { value in
resize(for: geo.size, amount: value)
}
}
}
/// Initialize a split view. This view isn't programmatically resizable; it can only be resized
/// by manually dragging the divider.
init(_ direction: SplitViewDirection, @ViewBuilder left: (() -> L), @ViewBuilder right: (() -> R)) {
self.init(
direction,
resizeIncrements: .init(width: 1, height: 1),
resizePublisher: .init(),
left: left,
right: right
)
}
/// Initialize a split view that supports programmatic resizing.
init(
_ direction: SplitViewDirection,
resizeIncrements: NSSize,
resizePublisher: PassthroughSubject<Double, Never>,
@ViewBuilder left: (() -> L),
@ViewBuilder right: (() -> R)
) {
self.direction = direction
self.resizeIncrements = resizeIncrements
self.resizePublisher = resizePublisher
self.left = left()
self.right = right()
}
private func resize(for size: CGSize, amount: Double) {
switch (direction) {
case .horizontal:
split += amount / size.width
case .vertical:
split += amount / size.height
}
// Ensure split is clamped between 0 and 1
split = max(0.0, min(split, 1.0))
}
private func dragGesture(_ size: CGSize, splitterPoint: CGPoint) -> some Gesture {
return DragGesture()
.onChanged { gesture in
@@ -72,10 +116,12 @@ struct SplitView<L: View, R: View>: View {
case .horizontal:
result.size.width = result.size.width * split
result.size.width -= splitterVisibleSize / 2
result.size.width -= result.size.width.truncatingRemainder(dividingBy: self.resizeIncrements.width)
case .vertical:
result.size.height = result.size.height * split
result.size.height -= splitterVisibleSize / 2
result.size.height -= result.size.height.truncatingRemainder(dividingBy: self.resizeIncrements.height)
}
return result

View File

@@ -19,6 +19,10 @@
<outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/>
<outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
<outlet property="menuIncreaseFontSize" destination="CIH-ey-Z6x" id="hkc-9C-80E"/>
<outlet property="menuMoveSplitDividerDown" destination="Zj7-2W-fdF" id="997-LL-nlN"/>
<outlet property="menuMoveSplitDividerLeft" destination="wSR-ny-j1a" id="HCZ-CI-2ob"/>
<outlet property="menuMoveSplitDividerRight" destination="CcX-ql-QU4" id="rIn-PK-fVM"/>
<outlet property="menuMoveSplitDividerUp" destination="h9Y-40-3oo" id="dDi-Vq-I3r"/>
<outlet property="menuNewTab" destination="uTG-Vz-hJU" id="eMg-R3-SeS"/>
<outlet property="menuNewWindow" destination="Was-JA-tGl" id="lK7-3I-CPG"/>
<outlet property="menuNextSplit" destination="bD7-ei-wKU" id="LeT-xw-eh4"/>
@@ -262,6 +266,37 @@
</items>
</menu>
</menuItem>
<menuItem title="Resize Split" id="BJO-3W-fkO" userLabel="Resize Split">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Resize Split" id="t7T-Ti-0im">
<items>
<menuItem title="Move Divider Up" id="h9Y-40-3oo">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="moveSplitDividerUp:" target="-1" id="NhD-6U-Eq2"/>
</connections>
</menuItem>
<menuItem title="Move Divider Down" id="Zj7-2W-fdF">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="moveSplitDividerDown:" target="-1" id="jeD-bm-wJX"/>
</connections>
</menuItem>
<menuItem title="Move Divider Left" id="wSR-ny-j1a">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="moveSplitDividerLeft:" target="-1" id="mlg-SJ-ZZO"/>
</connections>
</menuItem>
<menuItem title="Move Divider Right" id="CcX-ql-QU4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="moveSplitDividerRight:" target="-1" id="h3W-wY-PI7"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>