@@ -24,6 +24,46 @@ private enum WorkbenchWorkspaceMetrics {
2424 static let paneCornerRadius : CGFloat = 10
2525}
2626
27+ private enum WorkbenchPopoverLayoutMetrics {
28+ static let leadingOverlap : CGFloat = 10
29+ static let viewportMargin : CGFloat = 8
30+ static let arrowWidth : CGFloat = 22
31+ static let arrowHeight : CGFloat = 12
32+ }
33+
34+ private struct WorkbenchPopoverArrow : Shape {
35+ func path( in rect: CGRect ) -> Path {
36+ var path = Path ( )
37+ path. move ( to: CGPoint ( x: rect. minX, y: rect. maxY) )
38+ path. addLine ( to: CGPoint ( x: rect. midX, y: rect. minY) )
39+ path. addLine ( to: CGPoint ( x: rect. maxX, y: rect. maxY) )
40+ path. closeSubpath ( )
41+ return path
42+ }
43+ }
44+
45+ private struct ProjectSwitcherButtonBoundsPreferenceKey : PreferenceKey {
46+ static var defaultValue : Anchor < CGRect > ?
47+
48+ static func reduce(
49+ value: inout Anchor < CGRect > ? ,
50+ nextValue: ( ) -> Anchor < CGRect > ?
51+ ) {
52+ value = nextValue ( ) ?? value
53+ }
54+ }
55+
56+ private struct BranchSwitcherButtonBoundsPreferenceKey : PreferenceKey {
57+ static var defaultValue : Anchor < CGRect > ?
58+
59+ static func reduce(
60+ value: inout Anchor < CGRect > ? ,
61+ nextValue: ( ) -> Anchor < CGRect > ?
62+ ) {
63+ value = nextValue ( ) ?? value
64+ }
65+ }
66+
2767struct WorkbenchView : View {
2868 private let moduleUIRegistry = WorkbenchModuleUIComposition . builtIn
2969 @EnvironmentObject private var model : AppModel
@@ -234,26 +274,34 @@ struct WorkbenchView: View {
234274 } message: {
235275 Text ( model. pendingDiscardHunk? . change. path ?? " This action cannot be undone by Lithe. " )
236276 }
237- . confirmationDialog (
238- " Push ' \( pendingTopBarPushReference? . shortName ?? " " ) '? " ,
239- isPresented: Binding (
240- get: { pendingTopBarPushReference != nil } ,
241- set: { if !$0 { pendingTopBarPushReference = nil } }
242- ) ,
243- titleVisibility: . visible
244- ) {
245- Button ( " Push " ) {
246- guard let reference = pendingTopBarPushReference else { return }
247- pendingTopBarPushReference = nil
248- Task { await model. pushBranch ( reference) }
277+ . sheet ( item: $pendingTopBarPushReference) { reference in
278+ GitPushDialog (
279+ projectName: model. projectName,
280+ reference: reference,
281+ onPush: {
282+ Task { await model. pushBranch ( reference) }
283+ }
284+ )
285+ }
286+ . overlayPreferenceValue ( ProjectSwitcherButtonBoundsPreferenceKey . self) { bounds in
287+ GeometryReader { geometry in
288+ if isProjectSwitcherPresented, let bounds {
289+ projectSwitcherOverlay (
290+ buttonFrame: geometry [ bounds] ,
291+ viewportSize: geometry. size
292+ )
293+ }
249294 }
250- . lithePointer ( )
251- Button ( " Cancel " , role: . cancel) {
252- pendingTopBarPushReference = nil
295+ }
296+ . overlayPreferenceValue ( BranchSwitcherButtonBoundsPreferenceKey . self) { bounds in
297+ GeometryReader { geometry in
298+ if isBranchSwitcherPresented, let bounds {
299+ branchSwitcherOverlay (
300+ buttonFrame: geometry [ bounds] ,
301+ viewportSize: geometry. size
302+ )
303+ }
253304 }
254- . lithePointer ( )
255- } message: {
256- Text ( " This sends the current branch to its configured remote. " )
257305 }
258306 . overlay ( alignment: . bottom) {
259307 if let message = model. notificationMessage {
@@ -406,7 +454,10 @@ struct WorkbenchView: View {
406454 private var topBar : some View {
407455 HStack ( spacing: 9 ) {
408456 Button {
409- isProjectSwitcherPresented. toggle ( )
457+ updateSwitcherPresentation (
458+ project: !isProjectSwitcherPresented,
459+ branch: false
460+ )
410461 } label: {
411462 HStack ( spacing: 8 ) {
412463 LitheLogo ( size: 24 )
@@ -431,36 +482,21 @@ struct WorkbenchView: View {
431482 . buttonStyle ( . plain)
432483 . lithePointer ( )
433484 . accessibilityIdentifier ( " project-switcher- \( model. id. uuidString) " )
434- . popover ( isPresented: $isProjectSwitcherPresented, arrowEdge: . bottom) {
435- ProjectSwitcherPopover (
436- isPresented: $isProjectSwitcherPresented,
437- onNewProject: {
438- isProjectSwitcherPresented = false
439- model. chooseProject ( title: " New Project " , prompt: " Choose Folder " )
440- } ,
441- onOpenProject: {
442- isProjectSwitcherPresented = false
443- model. chooseProject ( )
444- } ,
445- onCloneRepository: {
446- isProjectSwitcherPresented = false
447- model. showCloneRepository ( )
448- } ,
449- onOpenRecentProject: { project in
450- isProjectSwitcherPresented = false
451- model. openProject ( project. url)
452- }
453- )
454- . environmentObject ( model)
455- }
485+ . anchorPreference (
486+ key: ProjectSwitcherButtonBoundsPreferenceKey . self,
487+ value: . bounds
488+ ) { $0 }
456489
457490 Rectangle ( )
458491 . fill ( LitheTheme . divider)
459492 . frame ( width: 1 , height: 20 )
460493 . padding ( . horizontal, 5 )
461494
462495 Button {
463- isBranchSwitcherPresented. toggle ( )
496+ updateSwitcherPresentation (
497+ project: false ,
498+ branch: !isBranchSwitcherPresented
499+ )
464500 if isBranchSwitcherPresented {
465501 Task { await model. refreshGitHistory ( ) }
466502 }
@@ -490,58 +526,206 @@ struct WorkbenchView: View {
490526 }
491527 . buttonStyle ( . plain)
492528 . lithePointer ( )
493- . popover ( isPresented: $isBranchSwitcherPresented, arrowEdge: . bottom) {
529+ . anchorPreference (
530+ key: BranchSwitcherButtonBoundsPreferenceKey . self,
531+ value: . bounds
532+ ) { $0 }
533+
534+ Spacer ( minLength: 22 )
535+
536+ runConfigurationPicker
537+ runLaunchButton
538+ debugLaunchButton
539+ if hasActiveExecution {
540+ stopExecutionButton
541+ }
542+
543+ backgroundPickerButton
544+
545+ }
546+ . padding ( . leading, 76 )
547+ . padding ( . trailing, 10 )
548+ . frame ( height: LitheTheme . Metrics. toolbarHeight)
549+ . background {
550+ ( model. workbenchBackgroundFeature. hasImage ? Color . clear : LitheTheme . titlebar)
551+ . contentShape ( Rectangle ( ) )
552+ . onTapGesture ( count: 2 ) {
553+ ( NSApplication . shared. keyWindow? . delegate as? LitheWindowCoordinator ) ?
554+ . toggleWorkspaceZoom ( )
555+ }
556+ }
557+ }
558+
559+ private func projectSwitcherOverlay(
560+ buttonFrame: CGRect ,
561+ viewportSize: CGSize
562+ ) -> some View {
563+ let popupMetrics = ProjectSwitcherLayoutMetrics . self
564+ let chromeMetrics = WorkbenchPopoverLayoutMetrics . self
565+ let placement = workbenchPopoverPlacement (
566+ buttonFrame: buttonFrame,
567+ viewportWidth: viewportSize. width,
568+ popupWidth: popupMetrics. width
569+ )
570+
571+ return ZStack ( alignment: . topLeading) {
572+ Color . clear
573+ . contentShape ( Rectangle ( ) )
574+ . onTapGesture { updateSwitcherPresentation ( project: false ) }
575+
576+ ZStack ( alignment: . topLeading) {
577+ WorkbenchPopoverArrow ( )
578+ . fill ( LitheTheme . popupBackground)
579+ . overlay {
580+ WorkbenchPopoverArrow ( )
581+ . stroke ( LitheTheme . panelBorder, lineWidth: 1 )
582+ }
583+ . frame ( width: chromeMetrics. arrowWidth, height: chromeMetrics. arrowHeight)
584+ . offset ( x: placement. arrowCenterX - ( chromeMetrics. arrowWidth / 2 ) )
585+
586+ ProjectSwitcherPopover (
587+ isPresented: instantProjectSwitcherPresentation,
588+ onNewProject: {
589+ updateSwitcherPresentation ( project: false )
590+ model. chooseProject ( title: " New Project " , prompt: " Choose Folder " )
591+ } ,
592+ onOpenProject: {
593+ updateSwitcherPresentation ( project: false )
594+ model. chooseProject ( )
595+ } ,
596+ onCloneRepository: {
597+ updateSwitcherPresentation ( project: false )
598+ model. showCloneRepository ( )
599+ } ,
600+ onOpenRecentProject: { project in
601+ updateSwitcherPresentation ( project: false )
602+ model. openProject ( project. url)
603+ }
604+ )
605+ . environmentObject ( model)
606+ . lithePopupChrome ( )
607+ . padding ( . top, chromeMetrics. arrowHeight - 1 )
608+ }
609+ . offset ( x: placement. popupX, y: buttonFrame. maxY)
610+ }
611+ . transaction { transaction in
612+ transaction. animation = nil
613+ transaction. disablesAnimations = true
614+ }
615+ . onExitCommand { updateSwitcherPresentation ( project: false ) }
616+ }
617+
618+ private func branchSwitcherOverlay(
619+ buttonFrame: CGRect ,
620+ viewportSize: CGSize
621+ ) -> some View {
622+ let popupMetrics = BranchSwitcherPopover . Metrics. self
623+ let chromeMetrics = WorkbenchPopoverLayoutMetrics . self
624+ let placement = workbenchPopoverPlacement (
625+ buttonFrame: buttonFrame,
626+ viewportWidth: viewportSize. width,
627+ popupWidth: popupMetrics. popupWidth
628+ )
629+
630+ return ZStack ( alignment: . topLeading) {
631+ Color . clear
632+ . contentShape ( Rectangle ( ) )
633+ . onTapGesture { updateSwitcherPresentation ( branch: false ) }
634+
635+ ZStack ( alignment: . topLeading) {
636+ WorkbenchPopoverArrow ( )
637+ . fill ( LitheTheme . popupBackground)
638+ . overlay {
639+ WorkbenchPopoverArrow ( )
640+ . stroke ( LitheTheme . panelBorder, lineWidth: 1 )
641+ }
642+ . frame ( width: chromeMetrics. arrowWidth, height: chromeMetrics. arrowHeight)
643+ . offset ( x: placement. arrowCenterX - ( chromeMetrics. arrowWidth / 2 ) )
644+
494645 BranchSwitcherPopover (
495- isPresented: $isBranchSwitcherPresented ,
646+ isPresented: instantBranchSwitcherPresentation ,
496647 onCommit: {
497- isBranchSwitcherPresented = false
648+ updateSwitcherPresentation ( branch : false )
498649 model. selectedSidebar = . changes
499650 } ,
500651 onPush: { reference in
501- isBranchSwitcherPresented = false
652+ updateSwitcherPresentation ( branch : false )
502653 pendingTopBarPushReference = reference
503654 } ,
504655 onNewBranch: { reference in
505- isBranchSwitcherPresented = false
656+ updateSwitcherPresentation ( branch : false )
506657 newBranchReference = reference
507658 } ,
508659 onCheckoutRevision: {
509- isBranchSwitcherPresented = false
660+ updateSwitcherPresentation ( branch : false )
510661 isCheckoutRevisionPresented = true
511662 } ,
512663 onManageBranches: {
513- isBranchSwitcherPresented = false
664+ updateSwitcherPresentation ( branch : false )
514665 if !model. isGitLogVisible {
515666 model. selectedSidebar = . changes
516667 Task { await model. toggleGitLog ( ) }
517668 }
518669 }
519670 )
520671 . environmentObject ( model)
672+ . padding ( . top, chromeMetrics. arrowHeight - 1 )
521673 }
674+ . offset ( x: placement. popupX, y: buttonFrame. maxY)
675+ }
676+ . transaction { transaction in
677+ transaction. animation = nil
678+ transaction. disablesAnimations = true
679+ }
680+ . onExitCommand { updateSwitcherPresentation ( branch: false ) }
681+ }
522682
523- Spacer ( minLength: 22 )
683+ private func workbenchPopoverPlacement(
684+ buttonFrame: CGRect ,
685+ viewportWidth: CGFloat ,
686+ popupWidth: CGFloat
687+ ) -> ( popupX: CGFloat , arrowCenterX: CGFloat ) {
688+ let metrics = WorkbenchPopoverLayoutMetrics . self
689+ let desiredX = buttonFrame. minX - metrics. leadingOverlap
690+ let maximumX = max (
691+ metrics. viewportMargin,
692+ viewportWidth - popupWidth - metrics. viewportMargin
693+ )
694+ let popupX = min ( max ( desiredX, metrics. viewportMargin) , maximumX)
695+ let arrowCenterX = min (
696+ max ( buttonFrame. midX - popupX, metrics. arrowWidth) ,
697+ popupWidth - metrics. arrowWidth
698+ )
699+ return ( popupX, arrowCenterX)
700+ }
524701
525- runConfigurationPicker
526- runLaunchButton
527- debugLaunchButton
528- if hasActiveExecution {
529- stopExecutionButton
530- }
702+ private var instantProjectSwitcherPresentation : Binding < Bool > {
703+ Binding (
704+ get : { isProjectSwitcherPresented } ,
705+ set : { updateSwitcherPresentation ( project : $0 ) }
706+ )
707+ }
531708
532- backgroundPickerButton
709+ private var instantBranchSwitcherPresentation : Binding < Bool > {
710+ Binding (
711+ get: { isBranchSwitcherPresented } ,
712+ set: { updateSwitcherPresentation ( branch: $0) }
713+ )
714+ }
533715
534- }
535- . padding ( . leading, 76 )
536- . padding ( . trailing, 10 )
537- . frame ( height: LitheTheme . Metrics. toolbarHeight)
538- . background {
539- ( model. workbenchBackgroundFeature. hasImage ? Color . clear : LitheTheme . titlebar)
540- . contentShape ( Rectangle ( ) )
541- . onTapGesture ( count: 2 ) {
542- ( NSApplication . shared. keyWindow? . delegate as? LitheWindowCoordinator ) ?
543- . toggleWorkspaceZoom ( )
544- }
716+ private func updateSwitcherPresentation(
717+ project: Bool ? = nil ,
718+ branch: Bool ? = nil
719+ ) {
720+ var transaction = Transaction ( animation: nil )
721+ transaction. disablesAnimations = true
722+ withTransaction ( transaction) {
723+ if let project {
724+ isProjectSwitcherPresented = project
725+ }
726+ if let branch {
727+ isBranchSwitcherPresented = branch
728+ }
545729 }
546730 }
547731
0 commit comments