mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
macos: only show split grab handle when the mouse is near it (#11383)
Fixes #11379 For this pass, I made it a very simple "within 20%" (height-wise) of the split handle down. There is no horizontal component. I want to find the right balance between always visible (today mostly) to only visible on direct hover, because I think it'll be too hard to discover on that far right side.
This commit is contained in:
@@ -3,6 +3,12 @@ import SwiftUI
|
||||
extension Ghostty {
|
||||
/// A grab handle overlay at the top of the surface for dragging a surface.
|
||||
struct SurfaceGrabHandle: View {
|
||||
// Size of the actual drag handle; the hover reveal region is larger.
|
||||
private static let handleSize = CGSize(width: 80, height: 12)
|
||||
|
||||
// Reveal the handle anywhere within the top % of the pane height.
|
||||
private static let hoverHeightFactor: CGFloat = 0.2
|
||||
|
||||
@ObservedObject var surfaceView: SurfaceView
|
||||
|
||||
@State private var isHovering: Bool = false
|
||||
@@ -19,7 +25,15 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
private var ellipsisVisible: Bool {
|
||||
surfaceView.mouseOverSurface && surfaceView.cursorVisible
|
||||
// If the cursor isn't visible, never show the handle
|
||||
guard surfaceView.cursorVisible else { return false }
|
||||
// If we're hovering or actively dragging, always visible
|
||||
if isHovering || isDragging { return true }
|
||||
|
||||
// Require our mouse location to be within the top area of the
|
||||
// surface.
|
||||
guard let mouseLocation = surfaceView.mouseLocationInSurface else { return false }
|
||||
return Self.isInHoverRegion(mouseLocation, in: surfaceView.bounds)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -30,7 +44,7 @@ extension Ghostty {
|
||||
isDragging: $isDragging,
|
||||
isHovering: $isHovering
|
||||
)
|
||||
.frame(width: 80, height: 12)
|
||||
.frame(width: Self.handleSize.width, height: Self.handleSize.height)
|
||||
.contentShape(Rectangle())
|
||||
|
||||
if ellipsisVisible {
|
||||
@@ -45,5 +59,23 @@ extension Ghostty {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
}
|
||||
|
||||
/// The full-width hover band that reveals the drag handle.
|
||||
private static func hoverRect(in bounds: CGRect) -> CGRect {
|
||||
guard !bounds.isEmpty else { return .zero }
|
||||
|
||||
let hoverHeight = min(bounds.height, max(handleSize.height, bounds.height * hoverHeightFactor))
|
||||
return CGRect(
|
||||
x: bounds.minX,
|
||||
y: bounds.maxY - hoverHeight,
|
||||
width: bounds.width,
|
||||
height: hoverHeight
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true when the pointer is inside the top hover band.
|
||||
private static func isInHoverRegion(_ point: CGPoint, in bounds: CGRect) -> Bool {
|
||||
hoverRect(in: bounds).contains(point)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,10 @@ extension Ghostty {
|
||||
// Whether the mouse is currently over this surface
|
||||
@Published private(set) var mouseOverSurface: Bool = false
|
||||
|
||||
// The last known mouse location in the surface's local coordinate space,
|
||||
// used by overlays such as the split drag handle reveal region.
|
||||
@Published private(set) var mouseLocationInSurface: CGPoint?
|
||||
|
||||
// Whether the cursor is currently visible (not hidden by typing, etc.)
|
||||
@Published private(set) var cursorVisible: Bool = true
|
||||
|
||||
@@ -952,13 +956,15 @@ extension Ghostty {
|
||||
mouseOverSurface = true
|
||||
super.mouseEntered(with: event)
|
||||
|
||||
let pos = self.convert(event.locationInWindow, from: nil)
|
||||
mouseLocationInSurface = pos
|
||||
|
||||
guard let surfaceModel else { return }
|
||||
|
||||
// On mouse enter we need to reset our cursor position. This is
|
||||
// super important because we set it to -1/-1 on mouseExit and
|
||||
// lots of mouse logic (i.e. whether to send mouse reports) depend
|
||||
// on the position being in the viewport if it is.
|
||||
let pos = self.convert(event.locationInWindow, from: nil)
|
||||
let mouseEvent = Ghostty.Input.MousePosEvent(
|
||||
x: pos.x,
|
||||
y: frame.height - pos.y,
|
||||
@@ -969,6 +975,7 @@ extension Ghostty {
|
||||
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
mouseOverSurface = false
|
||||
mouseLocationInSurface = nil
|
||||
guard let surfaceModel else { return }
|
||||
|
||||
// If the mouse is being dragged then we don't have to emit
|
||||
@@ -988,10 +995,12 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
override func mouseMoved(with event: NSEvent) {
|
||||
let pos = self.convert(event.locationInWindow, from: nil)
|
||||
mouseLocationInSurface = pos
|
||||
|
||||
guard let surfaceModel else { return }
|
||||
|
||||
// Convert window position to view position. Note (0, 0) is bottom left.
|
||||
let pos = self.convert(event.locationInWindow, from: nil)
|
||||
let mouseEvent = Ghostty.Input.MousePosEvent(
|
||||
x: pos.x,
|
||||
y: frame.height - pos.y,
|
||||
|
||||
Reference in New Issue
Block a user