detached 8da98d2

perf(threading): move UI operations off GTK main thread to eliminate hitches

commit
8da98d28e115d98a221594113e7215e50274ffd0
author
bamen · · 2025-09-16 19:27 UTC
committer
bamen · · 2025-09-16 17:19 UTC
parent
a732180
signature
unsigned
3 files · +86 -41
M internal/app/browser/webview.go +14 -3
@@ -66,6 +66,7 @@ func (app *BrowserApp) createWebView() error {
6666 DefaultMonospaceFont: app.config.Appearance.MonospaceFont,
6767 DefaultFontSize: app.config.Appearance.DefaultFontSize,
6868 Rendering: webkit.RenderingConfig{Mode: string(app.config.RenderingMode)},
69+ UseDomZoom: app.config.UseDomZoom,
6970 VideoAcceleration: webkit.VideoAccelerationConfig{
7071 EnableVAAPI: app.config.VideoAcceleration.EnableVAAPI,
7172 AutoDetectGPU: app.config.VideoAcceleration.AutoDetectGPU,
@@ -123,13 +124,19 @@ func (app *BrowserApp) setupWebViewIntegration() {
123124 func (app *BrowserApp) setupWebViewHandlers() {
124125 // Persist page titles to DB when they change
125126 app.webView.RegisterTitleChangedHandler(func(title string) {
126- ctx := context.Background()
127+ if title == "" {
128+ return
129+ }
127130 url := app.webView.GetCurrentURL()
128- if url != "" && title != "" {
131+ if url == "" {
132+ return
133+ }
134+ go func(url, title string) {
135+ ctx := context.Background()
129136 if err := app.browserService.UpdatePageTitle(ctx, url, title); err != nil {
130137 log.Printf("Warning: failed to update page title: %v", err)
131138 }
132- }
139+ }(url, title)
133140 })
134141
135142 // Set WebView reference for message handler and register script messages
@@ -150,6 +157,10 @@ func (app *BrowserApp) setupControllers() {
150157 )
151158 app.clipboardController = control.NewClipboardController(app.webView)
152159
160+ if app.messageHandler != nil && app.navigationController != nil {
161+ app.messageHandler.SetNavigationController(app.navigationController)
162+ }
163+
153164 // Register controller handlers
154165 app.zoomController.RegisterHandlers()
155166 }
@@ -66,6 +66,7 @@ func (app *BrowserApp) createWebView() error {
66 DefaultMonospaceFont: app.config.Appearance.MonospaceFont,66 DefaultMonospaceFont: app.config.Appearance.MonospaceFont,
67 DefaultFontSize: app.config.Appearance.DefaultFontSize,67 DefaultFontSize: app.config.Appearance.DefaultFontSize,
68 Rendering: webkit.RenderingConfig{Mode: string(app.config.RenderingMode)},68 Rendering: webkit.RenderingConfig{Mode: string(app.config.RenderingMode)},
69+ UseDomZoom: app.config.UseDomZoom,
69 VideoAcceleration: webkit.VideoAccelerationConfig{70 VideoAcceleration: webkit.VideoAccelerationConfig{
70 EnableVAAPI: app.config.VideoAcceleration.EnableVAAPI,71 EnableVAAPI: app.config.VideoAcceleration.EnableVAAPI,
71 AutoDetectGPU: app.config.VideoAcceleration.AutoDetectGPU,72 AutoDetectGPU: app.config.VideoAcceleration.AutoDetectGPU,
@@ -123,13 +124,19 @@ func (app *BrowserApp) setupWebViewIntegration() {
123 func (app *BrowserApp) setupWebViewHandlers() {124 func (app *BrowserApp) setupWebViewHandlers() {
124 // Persist page titles to DB when they change125 // Persist page titles to DB when they change
125 app.webView.RegisterTitleChangedHandler(func(title string) {126 app.webView.RegisterTitleChangedHandler(func(title string) {
126- ctx := context.Background()127+ if title == "" {
128+ return
129+ }
127 url := app.webView.GetCurrentURL()130 url := app.webView.GetCurrentURL()
128- if url != "" && title != "" {131+ if url == "" {
132+ return
133+ }
134+ go func(url, title string) {
135+ ctx := context.Background()
129 if err := app.browserService.UpdatePageTitle(ctx, url, title); err != nil {136 if err := app.browserService.UpdatePageTitle(ctx, url, title); err != nil {
130 log.Printf("Warning: failed to update page title: %v", err)137 log.Printf("Warning: failed to update page title: %v", err)
131 }138 }
132- }139+ }(url, title)
133 })140 })
134 141
135 // Set WebView reference for message handler and register script messages142 // Set WebView reference for message handler and register script messages
@@ -150,6 +157,10 @@ func (app *BrowserApp) setupControllers() {
150 )157 )
151 app.clipboardController = control.NewClipboardController(app.webView)158 app.clipboardController = control.NewClipboardController(app.webView)
152 159
160+ if app.messageHandler != nil && app.navigationController != nil {
161+ app.messageHandler.SetNavigationController(app.navigationController)
162+ }
163+
153 // Register controller handlers164 // Register controller handlers
154 app.zoomController.RegisterHandlers()165 app.zoomController.RegisterHandlers()
155 }166 }
M internal/app/control/zoom.go +64 -37
@@ -48,22 +48,8 @@ func (z *ZoomController) handleURIChange(url string) {
4848 if url == "" {
4949 return
5050 }
51- ctx := context.Background()
5251 currentDomain := services.ZoomKeyForLog(url)
53-
54- if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil {
55- z.programmaticChange = true
56- if err := z.webView.SetZoom(zoomLevel); err == nil {
57- log.Printf("[zoom] loaded %.2f for %s", zoomLevel, currentDomain)
58-
59- // Only show toast when entering a new domain (not on first load)
60- if currentDomain != z.lastZoomDomain && z.lastZoomDomain != "" {
61- z.showZoomToast(zoomLevel)
62- }
63- z.lastZoomDomain = currentDomain
64- }
65- z.programmaticChange = false
66- }
52+ z.loadZoomLevelAsync(url, currentDomain, true)
6753 }
6854
6955 // handleZoomChange responds to zoom level changes and persists them
@@ -72,13 +58,15 @@ func (z *ZoomController) handleZoomChange(level float64) {
7258 if url == "" {
7359 return
7460 }
75- ctx := context.Background()
76- if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil {
77- log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err)
78- return
79- }
80- key := services.ZoomKeyForLog(url)
81- log.Printf("[zoom] saved %.2f for %s", level, key)
61+ go func(url string, level float64) {
62+ ctx := context.Background()
63+ if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil {
64+ log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err)
65+ return
66+ }
67+ key := services.ZoomKeyForLog(url)
68+ log.Printf("[zoom] saved %.2f for %s", level, key)
69+ }(url, level)
8270
8371 // Only show toast for user-initiated zoom changes, not programmatic ones
8472 if !z.programmaticChange {
@@ -144,28 +132,67 @@ func (z *ZoomController) ApplyInitialZoom() {
144132 url = "dumb://homepage"
145133 }
146134 if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil {
147- z.programmaticChange = true
148- if err := z.webView.SetZoom(zoomLevel); err != nil {
149- log.Printf("Warning: failed to set initial zoom: %v", err)
150- } else {
151- key := services.ZoomKeyForLog(url)
152- log.Printf("[zoom] loaded %.2f for %s", zoomLevel, key)
153- }
154- z.programmaticChange = false
135+ z.applyZoomLevel(url, services.ZoomKeyForLog(url), zoomLevel, false)
155136 }
156137 }
157138
158139 // ApplyZoomForURL applies zoom for a specific URL (used for navigation)
159140 func (z *ZoomController) ApplyZoomForURL(url string) {
160- ctx := context.Background()
161- if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil {
141+ if url == "" {
142+ return
143+ }
144+ currentDomain := services.ZoomKeyForLog(url)
145+ z.loadZoomLevelAsync(url, currentDomain, false)
146+}
147+
148+// ApplyZoomForURLWithLevel applies a known zoom level without hitting the database again.
149+func (z *ZoomController) ApplyZoomForURLWithLevel(url string, zoomLevel float64, allowToast bool) {
150+ if z == nil || url == "" {
151+ return
152+ }
153+ z.applyZoomLevel(url, services.ZoomKeyForLog(url), zoomLevel, allowToast)
154+}
155+
156+func (z *ZoomController) loadZoomLevelAsync(url, domain string, allowToast bool) {
157+ if z.browserService == nil || z.webView == nil || url == "" {
158+ return
159+ }
160+ go func(url, domain string, allowToast bool) {
161+ ctx := context.Background()
162+ zoomLevel, err := z.browserService.GetZoomLevel(ctx, url)
163+ if err != nil {
164+ return
165+ }
166+
167+ z.applyZoomLevel(url, domain, zoomLevel, allowToast)
168+ }(url, domain, allowToast)
169+}
170+
171+func (z *ZoomController) applyZoomLevel(url, domain string, zoomLevel float64, allowToast bool) {
172+ if z == nil || z.webView == nil || url == "" {
173+ return
174+ }
175+
176+ z.webView.RunOnMainThread(func() {
177+ if z.webView == nil {
178+ return
179+ }
180+ if z.currentURL != url {
181+ z.currentURL = url
182+ }
183+
162184 z.programmaticChange = true
185+ defer func() { z.programmaticChange = false }()
186+
163187 if err := z.webView.SetZoom(zoomLevel); err != nil {
164188 log.Printf("Warning: failed to set zoom: %v", err)
165- } else {
166- key := services.ZoomKeyForLog(url)
167- log.Printf("[zoom] loaded %.2f for %s", zoomLevel, key)
189+ return
168190 }
169- z.programmaticChange = false
170- }
191+
192+ log.Printf("[zoom] loaded %.2f for %s", zoomLevel, domain)
193+ if allowToast && domain != z.lastZoomDomain && z.lastZoomDomain != "" {
194+ z.showZoomToast(zoomLevel)
195+ }
196+ z.lastZoomDomain = domain
197+ })
171198 }
@@ -48,22 +48,8 @@ func (z *ZoomController) handleURIChange(url string) {
48 if url == "" {48 if url == "" {
49 return49 return
50 }50 }
51- ctx := context.Background()
52 currentDomain := services.ZoomKeyForLog(url)51 currentDomain := services.ZoomKeyForLog(url)
53-52+ z.loadZoomLevelAsync(url, currentDomain, true)
54- if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil {
55- z.programmaticChange = true
56- if err := z.webView.SetZoom(zoomLevel); err == nil {
57- log.Printf("[zoom] loaded %.2f for %s", zoomLevel, currentDomain)
58-
59- // Only show toast when entering a new domain (not on first load)
60- if currentDomain != z.lastZoomDomain && z.lastZoomDomain != "" {
61- z.showZoomToast(zoomLevel)
62- }
63- z.lastZoomDomain = currentDomain
64- }
65- z.programmaticChange = false
66- }
67 }53 }
68 54
69 // handleZoomChange responds to zoom level changes and persists them55 // handleZoomChange responds to zoom level changes and persists them
@@ -72,13 +58,15 @@ func (z *ZoomController) handleZoomChange(level float64) {
72 if url == "" {58 if url == "" {
73 return59 return
74 }60 }
75- ctx := context.Background()61+ go func(url string, level float64) {
76- if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil {62+ ctx := context.Background()
77- log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err)63+ if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil {
78- return64+ log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err)
79- }65+ return
80- key := services.ZoomKeyForLog(url)66+ }
81- log.Printf("[zoom] saved %.2f for %s", level, key)67+ key := services.ZoomKeyForLog(url)
68+ log.Printf("[zoom] saved %.2f for %s", level, key)
69+ }(url, level)
82 70
83 // Only show toast for user-initiated zoom changes, not programmatic ones71 // Only show toast for user-initiated zoom changes, not programmatic ones
84 if !z.programmaticChange {72 if !z.programmaticChange {
@@ -144,28 +132,67 @@ func (z *ZoomController) ApplyInitialZoom() {
144 url = "dumb://homepage"132 url = "dumb://homepage"
145 }133 }
146 if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil {134 if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil {
147- z.programmaticChange = true135+ z.applyZoomLevel(url, services.ZoomKeyForLog(url), zoomLevel, false)
148- if err := z.webView.SetZoom(zoomLevel); err != nil {
149- log.Printf("Warning: failed to set initial zoom: %v", err)
150- } else {
151- key := services.ZoomKeyForLog(url)
152- log.Printf("[zoom] loaded %.2f for %s", zoomLevel, key)
153- }
154- z.programmaticChange = false
155 }136 }
156 }137 }
157 138
158 // ApplyZoomForURL applies zoom for a specific URL (used for navigation)139 // ApplyZoomForURL applies zoom for a specific URL (used for navigation)
159 func (z *ZoomController) ApplyZoomForURL(url string) {140 func (z *ZoomController) ApplyZoomForURL(url string) {
160- ctx := context.Background()141+ if url == "" {
161- if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil {142+ return
143+ }
144+ currentDomain := services.ZoomKeyForLog(url)
145+ z.loadZoomLevelAsync(url, currentDomain, false)
146+}
147+
148+// ApplyZoomForURLWithLevel applies a known zoom level without hitting the database again.
149+func (z *ZoomController) ApplyZoomForURLWithLevel(url string, zoomLevel float64, allowToast bool) {
150+ if z == nil || url == "" {
151+ return
152+ }
153+ z.applyZoomLevel(url, services.ZoomKeyForLog(url), zoomLevel, allowToast)
154+}
155+
156+func (z *ZoomController) loadZoomLevelAsync(url, domain string, allowToast bool) {
157+ if z.browserService == nil || z.webView == nil || url == "" {
158+ return
159+ }
160+ go func(url, domain string, allowToast bool) {
161+ ctx := context.Background()
162+ zoomLevel, err := z.browserService.GetZoomLevel(ctx, url)
163+ if err != nil {
164+ return
165+ }
166+
167+ z.applyZoomLevel(url, domain, zoomLevel, allowToast)
168+ }(url, domain, allowToast)
169+}
170+
171+func (z *ZoomController) applyZoomLevel(url, domain string, zoomLevel float64, allowToast bool) {
172+ if z == nil || z.webView == nil || url == "" {
173+ return
174+ }
175+
176+ z.webView.RunOnMainThread(func() {
177+ if z.webView == nil {
178+ return
179+ }
180+ if z.currentURL != url {
181+ z.currentURL = url
182+ }
183+
162 z.programmaticChange = true184 z.programmaticChange = true
185+ defer func() { z.programmaticChange = false }()
186+
163 if err := z.webView.SetZoom(zoomLevel); err != nil {187 if err := z.webView.SetZoom(zoomLevel); err != nil {
164 log.Printf("Warning: failed to set zoom: %v", err)188 log.Printf("Warning: failed to set zoom: %v", err)
165- } else {189+ return
166- key := services.ZoomKeyForLog(url)
167- log.Printf("[zoom] loaded %.2f for %s", zoomLevel, key)
168 }190 }
169- z.programmaticChange = false191+
170- }192+ log.Printf("[zoom] loaded %.2f for %s", zoomLevel, domain)
193+ if allowToast && domain != z.lastZoomDomain && z.lastZoomDomain != "" {
194+ z.showZoomToast(zoomLevel)
195+ }
196+ z.lastZoomDomain = domain
197+ })
171 }198 }
M services/browser_service.go +8 -1
@@ -215,7 +215,14 @@ func (s *BrowserService) UpdatePageTitle(ctx context.Context, url, title string)
215215 // Update the window title if we have a title updater
216216 if s.windowTitleUpdater != nil && title != "" {
217217 windowTitle := fmt.Sprintf("Dumber - %s", title)
218- s.windowTitleUpdater.SetTitle(windowTitle)
218+ updater := s.windowTitleUpdater
219+ if s.webView != nil {
220+ s.webView.RunOnMainThread(func() {
221+ updater.SetTitle(windowTitle)
222+ })
223+ } else {
224+ updater.SetTitle(windowTitle)
225+ }
219226 }
220227
221228 return nil
@@ -215,7 +215,14 @@ func (s *BrowserService) UpdatePageTitle(ctx context.Context, url, title string)
215 // Update the window title if we have a title updater215 // Update the window title if we have a title updater
216 if s.windowTitleUpdater != nil && title != "" {216 if s.windowTitleUpdater != nil && title != "" {
217 windowTitle := fmt.Sprintf("Dumber - %s", title)217 windowTitle := fmt.Sprintf("Dumber - %s", title)
218- s.windowTitleUpdater.SetTitle(windowTitle)218+ updater := s.windowTitleUpdater
219+ if s.webView != nil {
220+ s.webView.RunOnMainThread(func() {
221+ updater.SetTitle(windowTitle)
222+ })
223+ } else {
224+ updater.SetTitle(windowTitle)
225+ }
219 }226 }
220 227
221 return nil228 return nil

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close