Skip to content

Commit 6719995

Browse files
Fixed terminal resize and scroll layout issues, improved utility area toggle animation (#1845)
When the terminal was resized, it displayed partial lines at the bottom due to a remainder in the line height multiple. This caused two things: 1. Resizing the Utility Area drawer caused a visual stutter in the terminal. 2. Partial lines at the bottom created an unsightly visual artifact. The terminal height must now be a multiple of a single line’s height to resolve these issues. Utility area drawer now has a push/pull animation rather than a reveal. ### Screenshots Before https://github.com/user-attachments/assets/f092611e-fe28-4aeb-b4dd-408611f0229e https://github.com/user-attachments/assets/4c365d3d-e051-4bc4-86ef-7121e4b8a5a5 After https://github.com/user-attachments/assets/7fb1a8d0-1aac-42f2-8a0a-22c02a7511b0 VS Code handles this in a similar way https://github.com/user-attachments/assets/67507c80-e39e-45f4-8432-5e132df5116a Improved utility area animation https://github.com/user-attachments/assets/36825fd0-caa0-463e-a367-5c17cde7dce2 > [!NOTE] > I removed the maximize drawer toggle in favor of simply dragging to resize up. This simplifies things greatly. Co-authored-by: Tom Ludwig <tommludwig@icloud.com>
1 parent bf99e7d commit 6719995

File tree

6 files changed

+120
-65
lines changed

6 files changed

+120
-65
lines changed

CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import SwiftUI
9+
import Cocoa
910

1011
final class UtilityAreaTerminal: ObservableObject, Identifiable, Equatable {
1112
let id: UUID
@@ -36,6 +37,12 @@ struct UtilityAreaTerminalView: View {
3637
private var darkAppearance
3738
@AppSettings(\.theme.useThemeBackground)
3839
private var useThemeBackground
40+
@AppSettings(\.textEditing.font)
41+
private var textEditingFont
42+
@AppSettings(\.terminal.font)
43+
private var terminalFont
44+
@AppSettings(\.terminal.useTextEditorFont)
45+
private var useTextEditorFont
3946

4047
@Environment(\.colorScheme)
4148
private var colorScheme
@@ -52,6 +59,10 @@ struct UtilityAreaTerminalView: View {
5259

5360
@State private var popoverSource: CGRect = .zero
5461

62+
var font: NSFont {
63+
useTextEditorFont == true ? textEditingFont.current : terminalFont.current
64+
}
65+
5566
private func initializeTerminals() {
5667
let id = UUID()
5768

@@ -101,31 +112,52 @@ struct UtilityAreaTerminalView: View {
101112
utilityAreaViewModel.terminals.move(fromOffsets: source, toOffset: destination)
102113
}
103114

115+
func fontTotalHeight(nsFont: NSFont) -> CGFloat {
116+
let ctFont = nsFont as CTFont
117+
let ascent = CTFontGetAscent(ctFont)
118+
let descent = CTFontGetDescent(ctFont)
119+
let leading = CTFontGetLeading(ctFont)
120+
121+
return ascent + descent + leading
122+
}
123+
104124
var body: some View {
105125
UtilityAreaTabView(model: utilityAreaViewModel.tabViewModel) { tabState in
106126
ZStack {
107127
if utilityAreaViewModel.selectedTerminals.isEmpty {
108128
CEContentUnavailableView("No Selection")
109-
}
110-
ForEach(utilityAreaViewModel.terminals) { terminal in
111-
TerminalEmulatorView(
112-
url: terminal.url!,
113-
shellType: terminal.shell,
114-
onTitleChange: { [weak terminal] newTitle in
115-
guard let id = terminal?.id else { return }
116-
// This can be called whenever, even in a view update so it needs to be dispatched.
117-
DispatchQueue.main.async { [weak utilityAreaViewModel] in
118-
utilityAreaViewModel?.updateTerminal(id, title: newTitle)
129+
} else {
130+
GeometryReader { geometry in
131+
let containerHeight = geometry.size.height
132+
let totalFontHeight = fontTotalHeight(nsFont: font).rounded(.up)
133+
let constrainedHeight = containerHeight - containerHeight.truncatingRemainder(
134+
dividingBy: totalFontHeight
135+
)
136+
ForEach(utilityAreaViewModel.terminals) { terminal in
137+
VStack(spacing: 0) {
138+
Spacer(minLength: 0)
139+
.frame(minHeight: 0)
140+
TerminalEmulatorView(
141+
url: terminal.url!,
142+
shellType: terminal.shell,
143+
onTitleChange: { [weak terminal] newTitle in
144+
guard let id = terminal?.id else { return }
145+
// This can be called whenever, even in a view update
146+
// so it needs to be dispatched.
147+
DispatchQueue.main.async { [weak utilityAreaViewModel] in
148+
utilityAreaViewModel?.updateTerminal(id, title: newTitle)
149+
}
150+
}
151+
)
152+
.frame(height: constrainedHeight - totalFontHeight + 1)
119153
}
154+
.disabled(terminal.id != utilityAreaViewModel.selectedTerminals.first)
155+
.opacity(terminal.id == utilityAreaViewModel.selectedTerminals.first ? 1 : 0)
120156
}
121-
)
122-
.padding(.top, 10)
123-
.padding(.horizontal, 10)
124-
.contentShape(Rectangle())
125-
.disabled(terminal.id != utilityAreaViewModel.selectedTerminals.first)
126-
.opacity(terminal.id == utilityAreaViewModel.selectedTerminals.first ? 1 : 0)
157+
}
127158
}
128159
}
160+
.padding(.horizontal, 10)
129161
.paneToolbar {
130162
PaneToolbarSection {
131163
UtilityAreaTerminalPicker(

CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,8 @@ class UtilityAreaViewModel: ObservableObject {
5454
}
5555

5656
func togglePanel() {
57-
withAnimation {
58-
self.isCollapsed.toggle()
59-
}
57+
self.isMaximized = false
58+
self.isCollapsed.toggle()
6059
}
6160

6261
/// Update a terminal's title.

CodeEdit/Features/UtilityArea/Views/PaneToolbar.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ struct PaneToolbar<Content: View>: View {
2626
.frame(width: 24)
2727
}
2828
.opacity(0)
29-
Divider().opacity(0)
3029
}
3130
content
3231
if model.hasTrailingSidebar
@@ -35,16 +34,13 @@ struct PaneToolbar<Content: View>: View {
3534
&& model.trailingSidebarIsCollapsed)
3635
|| paneArea == .trailing
3736
) || !model.hasTrailingSidebar {
38-
Divider().opacity(0)
39-
PaneToolbarSection {
40-
if model.hasTrailingSidebar {
37+
if model.hasTrailingSidebar {
38+
PaneToolbarSection {
4139
Spacer()
4240
.frame(width: 24)
4341
}
44-
Spacer()
45-
.frame(width: 24)
42+
.opacity(0)
4643
}
47-
.opacity(0)
4844
}
4945
}
5046
.buttonStyle(.icon(size: 24))

CodeEdit/Features/UtilityArea/Views/UtilityAreaView.swift

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,28 +41,5 @@ struct UtilityAreaView: View {
4141
.overlay(Color(nsColor: colorScheme == .dark ? .black : .clear))
4242
}
4343
}
44-
.overlay(alignment: .bottomTrailing) {
45-
HStack(spacing: 5) {
46-
Divider()
47-
HStack(spacing: 0) {
48-
Button {
49-
utilityAreaViewModel.isMaximized.toggle()
50-
} label: {
51-
Image(systemName: "arrowtriangle.up.square")
52-
}
53-
.buttonStyle(.icon(isActive: utilityAreaViewModel.isMaximized, size: 24))
54-
}
55-
}
56-
.colorScheme(
57-
utilityAreaViewModel.selectedTerminals.isEmpty
58-
? colorScheme
59-
: matchAppearance && darkAppearance
60-
? themeModel.selectedDarkTheme?.appearance == .dark ? .dark : .light
61-
: themeModel.selectedTheme?.appearance == .dark ? .dark : .light
62-
)
63-
.padding(.horizontal, 5)
64-
.padding(.vertical, 8)
65-
.frame(maxHeight: 27)
66-
}
6744
}
6845
}

CodeEdit/WorkspaceView.swift

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ struct WorkspaceView: View {
2828
@State private var showingAlert = false
2929
@State private var terminalCollapsed = true
3030
@State private var editorCollapsed = false
31+
@State private var editorsHeight: CGFloat = 0
32+
@State private var drawerHeight: CGFloat = 0
33+
34+
private let statusbarHeight: CGFloat = 29
3135

3236
private var keybindings: KeybindingManager = .shared
3337

@@ -36,28 +40,75 @@ struct WorkspaceView: View {
3640
VStack {
3741
SplitViewReader { proxy in
3842
SplitView(axis: .vertical) {
39-
EditorLayoutView(
40-
layout: editorManager.isFocusingActiveEditor
41-
? editorManager.activeEditor.getEditorLayout() ?? editorManager.editorLayout
42-
: editorManager.editorLayout,
43-
focus: $focusedEditor
44-
)
43+
ZStack {
44+
GeometryReader { geo in
45+
EditorLayoutView(
46+
layout: editorManager.isFocusingActiveEditor
47+
? editorManager.activeEditor.getEditorLayout() ?? editorManager.editorLayout
48+
: editorManager.editorLayout,
49+
focus: $focusedEditor
50+
)
51+
.frame(maxWidth: .infinity, maxHeight: .infinity)
52+
.onChange(of: geo.size.height) { newHeight in
53+
editorsHeight = newHeight
54+
}
55+
.onAppear {
56+
editorsHeight = geo.size.height
57+
}
58+
}
59+
}
60+
.frame(minHeight: 170 + 29 + 29)
4561
.collapsable()
4662
.collapsed($utilityAreaViewModel.isMaximized)
47-
.frame(minHeight: 170 + 29 + 29)
48-
.frame(maxWidth: .infinity, maxHeight: .infinity)
4963
.holdingPriority(.init(1))
50-
.safeAreaInset(edge: .bottom, spacing: 0) {
51-
StatusBarView(proxy: proxy)
52-
}
53-
UtilityAreaView()
64+
Rectangle()
5465
.collapsable()
5566
.collapsed($utilityAreaViewModel.isCollapsed)
67+
.opacity(0)
5668
.frame(idealHeight: 260)
5769
.frame(minHeight: 100)
70+
.background {
71+
GeometryReader { geo in
72+
Rectangle()
73+
.opacity(0)
74+
.onChange(of: geo.size.height) { newHeight in
75+
drawerHeight = newHeight
76+
}
77+
.onAppear {
78+
drawerHeight = geo.size.height
79+
}
80+
}
81+
}
5882
}
5983
.edgesIgnoringSafeArea(.top)
6084
.frame(maxWidth: .infinity, maxHeight: .infinity)
85+
.overlay(alignment: .top) {
86+
ZStack(alignment: .top) {
87+
UtilityAreaView()
88+
.frame(height: utilityAreaViewModel.isMaximized ? nil : drawerHeight)
89+
.frame(maxHeight: utilityAreaViewModel.isMaximized ? .infinity : nil)
90+
.padding(.top, utilityAreaViewModel.isMaximized ? statusbarHeight + 1 : 0)
91+
.offset(y: utilityAreaViewModel.isMaximized ? 0 : editorsHeight + 1)
92+
VStack(spacing: 0) {
93+
StatusBarView(proxy: proxy)
94+
if utilityAreaViewModel.isMaximized {
95+
PanelDivider()
96+
}
97+
}
98+
.offset(y: utilityAreaViewModel.isMaximized ? 0 : editorsHeight - statusbarHeight)
99+
}
100+
}
101+
.onChange(of: focusedEditor) { newValue in
102+
/// update active tab group only if the new one is not the same with it.
103+
if let newValue, editorManager.activeEditor != newValue {
104+
editorManager.activeEditor = newValue
105+
}
106+
}
107+
.onChange(of: editorManager.activeEditor) { newValue in
108+
if newValue != focusedEditor {
109+
focusedEditor = newValue
110+
}
111+
}
61112
.task {
62113
themeModel.colorScheme = colorScheme
63114

0 commit comments

Comments
 (0)