|
@@ -629,7 +629,7 @@ private extension ViewController {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
|
|
/// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
|
|
|
-private final class RowHitTestView: NSView {
|
|
|
|
|
|
|
+private class RowHitTestView: NSView {
|
|
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
|
guard let superview else { return nil }
|
|
guard let superview else { return nil }
|
|
|
let local = convert(point, from: superview)
|
|
let local = convert(point, from: superview)
|
|
@@ -637,6 +637,50 @@ private final class RowHitTestView: NSView {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+private final class HoverTrackingView: RowHitTestView {
|
|
|
|
|
+ var onHoverChanged: ((Bool) -> Void)?
|
|
|
|
|
+ var showsHandCursor = true
|
|
|
|
|
+
|
|
|
|
|
+ private var trackingAreaRef: NSTrackingArea?
|
|
|
|
|
+ private var isHovering = false {
|
|
|
|
|
+ didSet {
|
|
|
|
|
+ guard isHovering != oldValue else { return }
|
|
|
|
|
+ onHoverChanged?(isHovering)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override func updateTrackingAreas() {
|
|
|
|
|
+ super.updateTrackingAreas()
|
|
|
|
|
+ if let trackingAreaRef {
|
|
|
|
|
+ removeTrackingArea(trackingAreaRef)
|
|
|
|
|
+ }
|
|
|
|
|
+ let options: NSTrackingArea.Options = [
|
|
|
|
|
+ .activeInKeyWindow,
|
|
|
|
|
+ .inVisibleRect,
|
|
|
|
|
+ .mouseEnteredAndExited
|
|
|
|
|
+ ]
|
|
|
|
|
+ let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
|
|
|
|
|
+ addTrackingArea(area)
|
|
|
|
|
+ trackingAreaRef = area
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override func mouseEntered(with event: NSEvent) {
|
|
|
|
|
+ super.mouseEntered(with: event)
|
|
|
|
|
+ isHovering = true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override func mouseExited(with event: NSEvent) {
|
|
|
|
|
+ super.mouseExited(with: event)
|
|
|
|
|
+ isHovering = false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override func resetCursorRects() {
|
|
|
|
|
+ super.resetCursorRects()
|
|
|
|
|
+ guard showsHandCursor else { return }
|
|
|
|
|
+ addCursorRect(bounds, cursor: .pointingHand)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
private final class SettingsMenuViewController: NSViewController {
|
|
private final class SettingsMenuViewController: NSViewController {
|
|
|
private let palette: Palette
|
|
private let palette: Palette
|
|
|
private let typography: Typography
|
|
private let typography: Typography
|
|
@@ -727,7 +771,7 @@ private final class SettingsMenuViewController: NSViewController {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func settingsDarkModeRow(enabled: Bool) -> NSView {
|
|
private func settingsDarkModeRow(enabled: Bool) -> NSView {
|
|
|
- let row = RowHitTestView()
|
|
|
|
|
|
|
+ let row = HoverTrackingView()
|
|
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
row.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
|
row.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
|
|
|
|
|
|
@@ -751,6 +795,12 @@ private final class SettingsMenuViewController: NSViewController {
|
|
|
row.addSubview(icon)
|
|
row.addSubview(icon)
|
|
|
row.addSubview(title)
|
|
row.addSubview(title)
|
|
|
row.addSubview(toggle)
|
|
row.addSubview(toggle)
|
|
|
|
|
+ row.onHoverChanged = { hovering in
|
|
|
|
|
+ row.wantsLayer = true
|
|
|
|
|
+ row.layer?.cornerRadius = 10
|
|
|
|
|
+ row.layer?.backgroundColor = (hovering ? NSColor(calibratedWhite: 1, alpha: 0.06) : NSColor.clear).cgColor
|
|
|
|
|
+ }
|
|
|
|
|
+ row.onHoverChanged?(false)
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
NSLayoutConstraint.activate([
|
|
|
icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
|
|
icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
|
|
@@ -767,7 +817,7 @@ private final class SettingsMenuViewController: NSViewController {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func settingsActionRow(icon: String, title: String, action: SettingsAction) -> NSView {
|
|
private func settingsActionRow(icon: String, title: String, action: SettingsAction) -> NSView {
|
|
|
- let row = RowHitTestView()
|
|
|
|
|
|
|
+ let row = HoverTrackingView()
|
|
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
row.heightAnchor.constraint(equalToConstant: 42).isActive = true
|
|
row.heightAnchor.constraint(equalToConstant: 42).isActive = true
|
|
|
|
|
|
|
@@ -794,6 +844,12 @@ private final class SettingsMenuViewController: NSViewController {
|
|
|
let click = NSClickGestureRecognizer(target: self, action: #selector(settingsActionClicked(_:)))
|
|
let click = NSClickGestureRecognizer(target: self, action: #selector(settingsActionClicked(_:)))
|
|
|
row.addGestureRecognizer(click)
|
|
row.addGestureRecognizer(click)
|
|
|
row.identifier = NSUserInterfaceItemIdentifier(rawValue: "\(action.rawValue)")
|
|
row.identifier = NSUserInterfaceItemIdentifier(rawValue: "\(action.rawValue)")
|
|
|
|
|
+ row.onHoverChanged = { hovering in
|
|
|
|
|
+ row.wantsLayer = true
|
|
|
|
|
+ row.layer?.cornerRadius = 10
|
|
|
|
|
+ row.layer?.backgroundColor = (hovering ? NSColor(calibratedWhite: 1, alpha: 0.06) : NSColor.clear).cgColor
|
|
|
|
|
+ }
|
|
|
|
|
+ row.onHoverChanged?(false)
|
|
|
|
|
|
|
|
return row
|
|
return row
|
|
|
}
|
|
}
|
|
@@ -852,7 +908,7 @@ private extension ViewController {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func sidebarItem(_ text: String, icon: String, page: SidebarPage, logoImageName: String? = nil, logoIconWidth: CGFloat = 18, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true, showsDisclosure: Bool = false) -> NSView {
|
|
func sidebarItem(_ text: String, icon: String, page: SidebarPage, logoImageName: String? = nil, logoIconWidth: CGFloat = 18, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true, showsDisclosure: Bool = false) -> NSView {
|
|
|
- let item = RowHitTestView()
|
|
|
|
|
|
|
+ let item = HoverTrackingView()
|
|
|
item.wantsLayer = true
|
|
item.wantsLayer = true
|
|
|
item.layer?.cornerRadius = 10
|
|
item.layer?.cornerRadius = 10
|
|
|
item.layer?.backgroundColor = NSColor.clear.cgColor
|
|
item.layer?.backgroundColor = NSColor.clear.cgColor
|
|
@@ -910,6 +966,10 @@ private extension ViewController {
|
|
|
NSLayoutConstraint.activate(constraints)
|
|
NSLayoutConstraint.activate(constraints)
|
|
|
|
|
|
|
|
applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate)
|
|
applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate)
|
|
|
|
|
+ item.onHoverChanged = { [weak self, weak item] hovering in
|
|
|
|
|
+ guard let self, let item else { return }
|
|
|
|
|
+ self.applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate, hovering: hovering)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
let click = NSClickGestureRecognizer(target: self, action: #selector(sidebarItemClicked(_:)))
|
|
let click = NSClickGestureRecognizer(target: self, action: #selector(sidebarItemClicked(_:)))
|
|
|
item.addGestureRecognizer(click)
|
|
item.addGestureRecognizer(click)
|
|
@@ -917,9 +977,10 @@ private extension ViewController {
|
|
|
return item
|
|
return item
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- func applySidebarRowStyle(_ item: NSView, page: SidebarPage, logoTemplate: Bool) {
|
|
|
|
|
|
|
+ func applySidebarRowStyle(_ item: NSView, page: SidebarPage, logoTemplate: Bool, hovering: Bool = false) {
|
|
|
let selected = (page == selectedSidebarPage)
|
|
let selected = (page == selectedSidebarPage)
|
|
|
- item.layer?.backgroundColor = (selected ? palette.primaryBlue : NSColor.clear).cgColor
|
|
|
|
|
|
|
+ let hoverColor = NSColor(calibratedWhite: 1, alpha: 0.07)
|
|
|
|
|
+ item.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor
|
|
|
let tint = selected ? NSColor.white : palette.textSecondary
|
|
let tint = selected ? NSColor.white : palette.textSecondary
|
|
|
guard item.subviews.count >= 2 else { return }
|
|
guard item.subviews.count >= 2 else { return }
|
|
|
let leading = item.subviews[0]
|
|
let leading = item.subviews[0]
|
|
@@ -939,7 +1000,7 @@ private extension ViewController {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func topTab(_ title: String, icon: String, provider: MeetingProvider, logoImageName: String? = nil, logoPointSize: CGFloat = 26, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true) -> NSView {
|
|
func topTab(_ title: String, icon: String, provider: MeetingProvider, logoImageName: String? = nil, logoPointSize: CGFloat = 26, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true) -> NSView {
|
|
|
- let tab = RowHitTestView()
|
|
|
|
|
|
|
+ let tab = HoverTrackingView()
|
|
|
tab.wantsLayer = true
|
|
tab.wantsLayer = true
|
|
|
tab.layer?.cornerRadius = 19
|
|
tab.layer?.cornerRadius = 19
|
|
|
tab.layer?.backgroundColor = NSColor.clear.cgColor
|
|
tab.layer?.backgroundColor = NSColor.clear.cgColor
|
|
@@ -981,6 +1042,10 @@ private extension ViewController {
|
|
|
NSLayoutConstraint.activate(constraints)
|
|
NSLayoutConstraint.activate(constraints)
|
|
|
|
|
|
|
|
applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate)
|
|
applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate)
|
|
|
|
|
+ tab.onHoverChanged = { [weak self, weak tab] hovering in
|
|
|
|
|
+ guard let self, let tab else { return }
|
|
|
|
|
+ self.applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate, hovering: hovering)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
let click = NSClickGestureRecognizer(target: self, action: #selector(meetingTabClicked(_:)))
|
|
let click = NSClickGestureRecognizer(target: self, action: #selector(meetingTabClicked(_:)))
|
|
|
tab.addGestureRecognizer(click)
|
|
tab.addGestureRecognizer(click)
|
|
@@ -988,9 +1053,10 @@ private extension ViewController {
|
|
|
return tab
|
|
return tab
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- func applyTabStyle(_ tab: NSView, provider: MeetingProvider, logoTemplate: Bool) {
|
|
|
|
|
|
|
+ func applyTabStyle(_ tab: NSView, provider: MeetingProvider, logoTemplate: Bool, hovering: Bool = false) {
|
|
|
let selected = (provider == selectedMeetingProvider)
|
|
let selected = (provider == selectedMeetingProvider)
|
|
|
- tab.layer?.backgroundColor = (selected ? palette.primaryBlue : NSColor.clear).cgColor
|
|
|
|
|
|
|
+ let hoverColor = NSColor(calibratedWhite: 1, alpha: 0.07)
|
|
|
|
|
+ tab.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor
|
|
|
guard tab.subviews.count >= 2 else { return }
|
|
guard tab.subviews.count >= 2 else { return }
|
|
|
let leading = tab.subviews[0]
|
|
let leading = tab.subviews[0]
|
|
|
let title = tab.subviews[1] as? NSTextField
|
|
let title = tab.subviews[1] as? NSTextField
|
|
@@ -1006,7 +1072,10 @@ private extension ViewController {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func actionButton(title: String, color: NSColor, textColor: NSColor, width: CGFloat) -> NSView {
|
|
func actionButton(title: String, color: NSColor, textColor: NSColor, width: CGFloat) -> NSView {
|
|
|
- let button = roundedContainer(cornerRadius: 9, color: color)
|
|
|
|
|
|
|
+ let button = HoverTrackingView()
|
|
|
|
|
+ button.wantsLayer = true
|
|
|
|
|
+ button.layer?.cornerRadius = 9
|
|
|
|
|
+ button.layer?.backgroundColor = color.cgColor
|
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
button.widthAnchor.constraint(equalToConstant: width).isActive = true
|
|
button.widthAnchor.constraint(equalToConstant: width).isActive = true
|
|
|
button.heightAnchor.constraint(equalToConstant: 36).isActive = true
|
|
button.heightAnchor.constraint(equalToConstant: 36).isActive = true
|
|
@@ -1022,11 +1091,21 @@ private extension ViewController {
|
|
|
label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
|
|
label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
|
|
|
])
|
|
])
|
|
|
|
|
|
|
|
|
|
+ let baseColor = (title == "Cancel") ? palette.cancelButton : color
|
|
|
|
|
+ let hoverColor = baseColor.blended(withFraction: 0.12, of: NSColor.white) ?? baseColor
|
|
|
|
|
+ button.onHoverChanged = { hovering in
|
|
|
|
|
+ button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
|
|
|
|
|
+ }
|
|
|
|
|
+ button.onHoverChanged?(false)
|
|
|
|
|
+
|
|
|
return button
|
|
return button
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func iconRoundButton(_ symbol: String, size: CGFloat) -> NSView {
|
|
func iconRoundButton(_ symbol: String, size: CGFloat) -> NSView {
|
|
|
- let button = roundedContainer(cornerRadius: size / 2, color: palette.inputBackground)
|
|
|
|
|
|
|
+ let button = HoverTrackingView()
|
|
|
|
|
+ button.wantsLayer = true
|
|
|
|
|
+ button.layer?.cornerRadius = size / 2
|
|
|
|
|
+ button.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
button.widthAnchor.constraint(equalToConstant: size).isActive = true
|
|
button.widthAnchor.constraint(equalToConstant: size).isActive = true
|
|
|
button.heightAnchor.constraint(equalToConstant: size).isActive = true
|
|
button.heightAnchor.constraint(equalToConstant: size).isActive = true
|
|
@@ -1039,6 +1118,13 @@ private extension ViewController {
|
|
|
label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
|
|
label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
|
|
|
])
|
|
])
|
|
|
|
|
|
|
|
|
|
+ let baseColor = palette.inputBackground
|
|
|
|
|
+ let hoverColor = baseColor.blended(withFraction: 0.10, of: NSColor.white) ?? baseColor
|
|
|
|
|
+ button.onHoverChanged = { hovering in
|
|
|
|
|
+ button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
|
|
|
|
|
+ }
|
|
|
|
|
+ button.onHoverChanged?(false)
|
|
|
|
|
+
|
|
|
return button
|
|
return button
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|