detached 845a627

Merge branch 'main' into chore/diagnostic-window-launch-logs

commit
845a627100c1dfebaca5f1b2a2ffc28df2e2097b
author
bnema · · 2026-06-09 04:26 UTC
committer
GitHub · · 2026-06-09 04:26 UTC
merge of
2330713 + 9d58593 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
19 files · +196 -227
M docs/config/index.md +4 -2
@@ -195,14 +195,16 @@ border = "#363636"
195195 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 | Explicit CEF OSR FPS cap; 0 uses adaptive mode if enabled, otherwise CEF default |
196196 | `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 | Hard cap for adaptive CEF OSR FPS; 0 falls back to the built-in default cap selected by Dumber/CEF |
197197 | `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | > 0 | Mouse wheel scroll sensitivity multiplier |
198-| `engine.cef.input.scroll_touchpad_multiplier` | float | `0.35` | > 0 | Touchpad/smooth scroll sensitivity multiplier |
198+| `engine.cef.input.scroll_precise_multiplier` | float | `2.5` | > 0 | Precise/surface scroll sensitivity multiplier |
199199 | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 | Horizontal scroll sensitivity multiplier |
200200 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 | Vertical scroll sensitivity multiplier |
201201 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 | Maximum absolute scroll delta after scaling; 0 disables clamping |
202202 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | - | Enable two-finger touchpad swipe back/forward navigation |
203-| `engine.cef.input.touchpad_navigation_min_delta` | float | `80.0` | > 0 | Minimum accumulated horizontal swipe delta required for navigation |
203+| `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | > 0 | Minimum accumulated horizontal swipe delta required for navigation |
204204 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 | Maximum vertical-to-horizontal delta ratio allowed for navigation swipes |
205205 | `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |
206+
207+`engine.cef.input.touchpad_navigation_min_delta` uses raw GTK touchpad surface units for back/forward gestures. The default `200.0` matches WebKit-style commit distance to reduce accidental navigation; raise or lower it in `config.toml` to tune gesture sensitivity.
206208 | `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |
207209
208210 ## Workspace Configuration
@@ -195,14 +195,16 @@ border = "#363636"
195 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 | Explicit CEF OSR FPS cap; 0 uses adaptive mode if enabled, otherwise CEF default |195 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 | Explicit CEF OSR FPS cap; 0 uses adaptive mode if enabled, otherwise CEF default |
196 | `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 | Hard cap for adaptive CEF OSR FPS; 0 falls back to the built-in default cap selected by Dumber/CEF |196 | `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 | Hard cap for adaptive CEF OSR FPS; 0 falls back to the built-in default cap selected by Dumber/CEF |
197 | `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | > 0 | Mouse wheel scroll sensitivity multiplier |197 | `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | > 0 | Mouse wheel scroll sensitivity multiplier |
198-| `engine.cef.input.scroll_touchpad_multiplier` | float | `0.35` | > 0 | Touchpad/smooth scroll sensitivity multiplier |198+| `engine.cef.input.scroll_precise_multiplier` | float | `2.5` | > 0 | Precise/surface scroll sensitivity multiplier |
199 | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 | Horizontal scroll sensitivity multiplier |199 | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 | Horizontal scroll sensitivity multiplier |
200 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 | Vertical scroll sensitivity multiplier |200 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 | Vertical scroll sensitivity multiplier |
201 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 | Maximum absolute scroll delta after scaling; 0 disables clamping |201 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 | Maximum absolute scroll delta after scaling; 0 disables clamping |
202 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | - | Enable two-finger touchpad swipe back/forward navigation |202 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | - | Enable two-finger touchpad swipe back/forward navigation |
203-| `engine.cef.input.touchpad_navigation_min_delta` | float | `80.0` | > 0 | Minimum accumulated horizontal swipe delta required for navigation |203+| `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | > 0 | Minimum accumulated horizontal swipe delta required for navigation |
204 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 | Maximum vertical-to-horizontal delta ratio allowed for navigation swipes |204 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 | Maximum vertical-to-horizontal delta ratio allowed for navigation swipes |
205 | `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |205 | `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |
206+
207+`engine.cef.input.touchpad_navigation_min_delta` uses raw GTK touchpad surface units for back/forward gestures. The default `200.0` matches WebKit-style commit distance to reduce accidental navigation; raise or lower it in `config.toml` to tune gesture sensitivity.
206 | `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |208 | `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |
207 209
208 ## Workspace Configuration210 ## Workspace Configuration
M docs/reference/configuration.md +4 -2
@@ -65,14 +65,16 @@
6565 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |
6666 | `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 |
6767 | `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | > 0 |
68-| `engine.cef.input.scroll_touchpad_multiplier` | float | `0.35` | > 0 |
68+| `engine.cef.input.scroll_precise_multiplier` | float | `2.5` | > 0 |
6969 | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 |
7070 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 |
7171 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 |
7272 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | |
73-| `engine.cef.input.touchpad_navigation_min_delta` | float | `80.0` | > 0 |
73+| `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | > 0 |
7474 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 |
7575 | `default_ui_scale` | float | `1.0` | > 0 |
76+
77+`engine.cef.input.touchpad_navigation_min_delta` is measured in raw GTK touchpad surface units. The default `200.0` matches WebKit-style commit distance to reduce accidental back/forward navigation and can be overridden in `config.toml`.
7678 | `default_webpage_zoom` | float | `1.2` | > 0 |
7779 | `workspace.new_pane_url` | string | `about:blank` | |
7880 | `workspace.switch_to_tab_on_move` | bool | `true` | |
@@ -65,14 +65,16 @@
65 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |65 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |
66 | `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 |66 | `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 |
67 | `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | > 0 |67 | `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | > 0 |
68-| `engine.cef.input.scroll_touchpad_multiplier` | float | `0.35` | > 0 |68+| `engine.cef.input.scroll_precise_multiplier` | float | `2.5` | > 0 |
69 | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 |69 | `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 |
70 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 |70 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 |
71 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 |71 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 |
72 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | |72 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | |
73-| `engine.cef.input.touchpad_navigation_min_delta` | float | `80.0` | > 0 |73+| `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | > 0 |
74 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 |74 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 |
75 | `default_ui_scale` | float | `1.0` | > 0 |75 | `default_ui_scale` | float | `1.0` | > 0 |
76+
77+`engine.cef.input.touchpad_navigation_min_delta` is measured in raw GTK touchpad surface units. The default `200.0` matches WebKit-style commit distance to reduce accidental back/forward navigation and can be overridden in `config.toml`.
76 | `default_webpage_zoom` | float | `1.2` | > 0 |78 | `default_webpage_zoom` | float | `1.2` | > 0 |
77 | `workspace.new_pane_url` | string | `about:blank` | |79 | `workspace.new_pane_url` | string | `about:blank` | |
78 | `workspace.switch_to_tab_on_move` | bool | `true` | |80 | `workspace.switch_to_tab_on_move` | bool | `true` | |
M go.mod +1 -1
@@ -7,7 +7,7 @@ require (
77 github.com/andybalholm/brotli v1.2.1
88 github.com/bnema/purego v0.11.0-bnema.3
99 github.com/bnema/purego-cef v0.13.2
10- github.com/bnema/purego-cef2gtk v0.7.0
10+ github.com/bnema/purego-cef2gtk v0.8.0
1111 github.com/bnema/purego-pipewire v0.1.4
1212 github.com/bnema/purego-sqlite v0.1.3
1313 github.com/bnema/puregotk v0.6.0
@@ -7,7 +7,7 @@ require (
7 github.com/andybalholm/brotli v1.2.17 github.com/andybalholm/brotli v1.2.1
8 github.com/bnema/purego v0.11.0-bnema.38 github.com/bnema/purego v0.11.0-bnema.3
9 github.com/bnema/purego-cef v0.13.29 github.com/bnema/purego-cef v0.13.2
10- github.com/bnema/purego-cef2gtk v0.7.010+ github.com/bnema/purego-cef2gtk v0.8.0
11 github.com/bnema/purego-pipewire v0.1.411 github.com/bnema/purego-pipewire v0.1.4
12 github.com/bnema/purego-sqlite v0.1.312 github.com/bnema/purego-sqlite v0.1.3
13 github.com/bnema/puregotk v0.6.013 github.com/bnema/puregotk v0.6.0
M go.sum +2 -2
@@ -14,8 +14,8 @@ github.com/bnema/purego v0.11.0-bnema.3 h1:lpTXuapRIBH2OTgLJTjYlK1olHmZkEQLloqgP
1414 github.com/bnema/purego v0.11.0-bnema.3/go.mod h1:5BnOIUj+0fvk/vwv45iottFoGfkDT7cnLXnpM2s6Hng=
1515 github.com/bnema/purego-cef v0.13.2 h1:weSttuJVxjyzp/A5J15ZdzOAVWO/jvvd6nBf8/N4zsI=
1616 github.com/bnema/purego-cef v0.13.2/go.mod h1:Jbc4GgUvGakEOfbRnXJGYrDbXz03YEpVgTWKPk5ss9k=
17-github.com/bnema/purego-cef2gtk v0.7.0 h1:sv8VUB8VIqwrwV9M5E2u6pZ24ly4+erCeooRyCMz1mc=
18-github.com/bnema/purego-cef2gtk v0.7.0/go.mod h1:eLgf2GwRirBmRNzPWAp/VMHf4MyAN8xoWbdoUNgW48A=
17+github.com/bnema/purego-cef2gtk v0.8.0 h1:Fd1umUyyPuIqmqyg252dChcv6XJtYVsudleEZ7we9LY=
18+github.com/bnema/purego-cef2gtk v0.8.0/go.mod h1:eLgf2GwRirBmRNzPWAp/VMHf4MyAN8xoWbdoUNgW48A=
1919 github.com/bnema/purego-pipewire v0.1.4 h1:ThuPiZo9BeYn1w06hg/g0INgQUIBi8n9Gq1o5VRtjxk=
2020 github.com/bnema/purego-pipewire v0.1.4/go.mod h1:dn9RGRtNteqIarb8gTv97VD/FQgur7ZJ1BE/Ub9NYSk=
2121 github.com/bnema/purego-sqlite v0.1.3 h1:aXysq9+wuRkbFzlVe2oIiooYseFZFb6ArwbfDYYafvY=
@@ -14,8 +14,8 @@ github.com/bnema/purego v0.11.0-bnema.3 h1:lpTXuapRIBH2OTgLJTjYlK1olHmZkEQLloqgP
14 github.com/bnema/purego v0.11.0-bnema.3/go.mod h1:5BnOIUj+0fvk/vwv45iottFoGfkDT7cnLXnpM2s6Hng=14 github.com/bnema/purego v0.11.0-bnema.3/go.mod h1:5BnOIUj+0fvk/vwv45iottFoGfkDT7cnLXnpM2s6Hng=
15 github.com/bnema/purego-cef v0.13.2 h1:weSttuJVxjyzp/A5J15ZdzOAVWO/jvvd6nBf8/N4zsI=15 github.com/bnema/purego-cef v0.13.2 h1:weSttuJVxjyzp/A5J15ZdzOAVWO/jvvd6nBf8/N4zsI=
16 github.com/bnema/purego-cef v0.13.2/go.mod h1:Jbc4GgUvGakEOfbRnXJGYrDbXz03YEpVgTWKPk5ss9k=16 github.com/bnema/purego-cef v0.13.2/go.mod h1:Jbc4GgUvGakEOfbRnXJGYrDbXz03YEpVgTWKPk5ss9k=
17-github.com/bnema/purego-cef2gtk v0.7.0 h1:sv8VUB8VIqwrwV9M5E2u6pZ24ly4+erCeooRyCMz1mc=17+github.com/bnema/purego-cef2gtk v0.8.0 h1:Fd1umUyyPuIqmqyg252dChcv6XJtYVsudleEZ7we9LY=
18-github.com/bnema/purego-cef2gtk v0.7.0/go.mod h1:eLgf2GwRirBmRNzPWAp/VMHf4MyAN8xoWbdoUNgW48A=18+github.com/bnema/purego-cef2gtk v0.8.0/go.mod h1:eLgf2GwRirBmRNzPWAp/VMHf4MyAN8xoWbdoUNgW48A=
19 github.com/bnema/purego-pipewire v0.1.4 h1:ThuPiZo9BeYn1w06hg/g0INgQUIBi8n9Gq1o5VRtjxk=19 github.com/bnema/purego-pipewire v0.1.4 h1:ThuPiZo9BeYn1w06hg/g0INgQUIBi8n9Gq1o5VRtjxk=
20 github.com/bnema/purego-pipewire v0.1.4/go.mod h1:dn9RGRtNteqIarb8gTv97VD/FQgur7ZJ1BE/Ub9NYSk=20 github.com/bnema/purego-pipewire v0.1.4/go.mod h1:dn9RGRtNteqIarb8gTv97VD/FQgur7ZJ1BE/Ub9NYSk=
21 github.com/bnema/purego-sqlite v0.1.3 h1:aXysq9+wuRkbFzlVe2oIiooYseFZFb6ArwbfDYYafvY=21 github.com/bnema/purego-sqlite v0.1.3 h1:aXysq9+wuRkbFzlVe2oIiooYseFZFb6ArwbfDYYafvY=
M internal/application/usecase/favicon_test.go +50 -6
@@ -238,6 +238,36 @@ func TestFaviconRefreshFromIconURLsUsesPageURLKey(t *testing.T) {
238238 }
239239 }
240240
241+func TestFaviconRefreshFromIconURLsSkipsUnsupportedCandidates(t *testing.T) {
242+ fx := newFaviconFixture()
243+ fx.fetcher.responses = map[string]fakeFetchedIcon{
244+ "https://cdn.example.net/icon.svg": {bytes: []byte("svg"), contentType: "image/svg+xml"},
245+ "https://cdn.example.net/icon.png": {bytes: []byte("png"), contentType: "image/png"},
246+ }
247+ fx.converter.errByContentType = map[string]error{"image/svg+xml": ErrFaviconMiss}
248+
249+ err := fx.uc.RefreshFromIconURLs(context.Background(), "https://docs.example.com/page", []string{
250+ "https://cdn.example.net/icon.svg",
251+ "https://cdn.example.net/icon.png",
252+ })
253+ if err != nil {
254+ t.Fatal(err)
255+ }
256+ if fx.fetcher.calls != 2 {
257+ t.Fatalf("fetcher calls=%d, want 2", fx.fetcher.calls)
258+ }
259+ meta := fx.repo.byKey["docs.example.com"]
260+ if meta == nil {
261+ t.Fatalf("expected page URL key to be stored; keys=%v", fx.repo.byKey)
262+ }
263+ if meta.SourceURL != "https://cdn.example.net/icon.png" || meta.ContentType != "image/png" {
264+ t.Fatalf("stored meta = %+v, want png candidate", meta)
265+ }
266+ if string(fx.blobs.original["docs.example.com"]) != "png" {
267+ t.Fatalf("stored original = %q, want png", fx.blobs.original["docs.example.com"])
268+ }
269+}
270+
241271 func TestFaviconObserveConversionErrorDoesNotMutateExistingAssets(t *testing.T) {
242272 fx := newFaviconFixture()
243273 fx.seedOriginal([]byte("old"))
@@ -462,15 +492,19 @@ func (b *fakeBlobStore) RemoveDerived(_ context.Context, key favicon.Key) error
462492 }
463493
464494 type fakeConverter struct {
465- calls int
466- err error
495+ calls int
496+ err error
497+ errByContentType map[string]error
467498 }
468499
469-func (c *fakeConverter) Convert(_ context.Context, original []byte, _ string, sizes []int) (*appport.ConvertedFavicon, error) {
500+func (c *fakeConverter) Convert(_ context.Context, original []byte, contentType string, sizes []int) (*appport.ConvertedFavicon, error) {
470501 c.calls++
471502 if c.err != nil {
472503 return nil, c.err
473504 }
505+ if err := c.errByContentType[contentType]; err != nil {
506+ return nil, err
507+ }
474508 out := &appport.ConvertedFavicon{PNG: append([]byte("png:"), original...), SizedPNG: map[int][]byte{}}
475509 for _, s := range sizes {
476510 out.SizedPNG[s] = append([]byte("png32:"), original...)
@@ -514,9 +548,15 @@ func (i *fakeInvalidators) InvalidateAll(_ context.Context, _ favicon.Key) error
514548 return nil
515549 }
516550
551+type fakeFetchedIcon struct {
552+ bytes []byte
553+ contentType string
554+}
555+
517556 type fakeFetcher struct {
518- calls int
519- err error
557+ calls int
558+ err error
559+ responses map[string]fakeFetchedIcon
520560 }
521561
522562 func (f *fakeFetcher) Fetch(_ context.Context, req appport.FaviconFetchRequest) (*appport.FaviconFetchedIcon, error) {
@@ -528,5 +568,9 @@ func (f *fakeFetcher) Fetch(_ context.Context, req appport.FaviconFetchRequest)
528568 if iconURL == "" {
529569 iconURL = "https://example.com/favicon.ico"
530570 }
531- return &appport.FaviconFetchedIcon{PageURL: req.PageURL, IconURL: iconURL, Bytes: []byte("fetched"), Source: favicon.SourceDuckDuckGo, ContentType: "image/png"}, nil
571+ response := f.responses[iconURL]
572+ if response.bytes == nil {
573+ response = fakeFetchedIcon{bytes: []byte("fetched"), contentType: "image/png"}
574+ }
575+ return &appport.FaviconFetchedIcon{PageURL: req.PageURL, IconURL: iconURL, Bytes: response.bytes, Source: favicon.SourceDuckDuckGo, ContentType: response.contentType}, nil
532576 }
@@ -238,6 +238,36 @@ func TestFaviconRefreshFromIconURLsUsesPageURLKey(t *testing.T) {
238 }238 }
239 }239 }
240 240
241+func TestFaviconRefreshFromIconURLsSkipsUnsupportedCandidates(t *testing.T) {
242+ fx := newFaviconFixture()
243+ fx.fetcher.responses = map[string]fakeFetchedIcon{
244+ "https://cdn.example.net/icon.svg": {bytes: []byte("svg"), contentType: "image/svg+xml"},
245+ "https://cdn.example.net/icon.png": {bytes: []byte("png"), contentType: "image/png"},
246+ }
247+ fx.converter.errByContentType = map[string]error{"image/svg+xml": ErrFaviconMiss}
248+
249+ err := fx.uc.RefreshFromIconURLs(context.Background(), "https://docs.example.com/page", []string{
250+ "https://cdn.example.net/icon.svg",
251+ "https://cdn.example.net/icon.png",
252+ })
253+ if err != nil {
254+ t.Fatal(err)
255+ }
256+ if fx.fetcher.calls != 2 {
257+ t.Fatalf("fetcher calls=%d, want 2", fx.fetcher.calls)
258+ }
259+ meta := fx.repo.byKey["docs.example.com"]
260+ if meta == nil {
261+ t.Fatalf("expected page URL key to be stored; keys=%v", fx.repo.byKey)
262+ }
263+ if meta.SourceURL != "https://cdn.example.net/icon.png" || meta.ContentType != "image/png" {
264+ t.Fatalf("stored meta = %+v, want png candidate", meta)
265+ }
266+ if string(fx.blobs.original["docs.example.com"]) != "png" {
267+ t.Fatalf("stored original = %q, want png", fx.blobs.original["docs.example.com"])
268+ }
269+}
270+
241 func TestFaviconObserveConversionErrorDoesNotMutateExistingAssets(t *testing.T) {271 func TestFaviconObserveConversionErrorDoesNotMutateExistingAssets(t *testing.T) {
242 fx := newFaviconFixture()272 fx := newFaviconFixture()
243 fx.seedOriginal([]byte("old"))273 fx.seedOriginal([]byte("old"))
@@ -462,15 +492,19 @@ func (b *fakeBlobStore) RemoveDerived(_ context.Context, key favicon.Key) error
462 }492 }
463 493
464 type fakeConverter struct {494 type fakeConverter struct {
465- calls int495+ calls int
466- err error496+ err error
497+ errByContentType map[string]error
467 }498 }
468 499
469-func (c *fakeConverter) Convert(_ context.Context, original []byte, _ string, sizes []int) (*appport.ConvertedFavicon, error) {500+func (c *fakeConverter) Convert(_ context.Context, original []byte, contentType string, sizes []int) (*appport.ConvertedFavicon, error) {
470 c.calls++501 c.calls++
471 if c.err != nil {502 if c.err != nil {
472 return nil, c.err503 return nil, c.err
473 }504 }
505+ if err := c.errByContentType[contentType]; err != nil {
506+ return nil, err
507+ }
474 out := &appport.ConvertedFavicon{PNG: append([]byte("png:"), original...), SizedPNG: map[int][]byte{}}508 out := &appport.ConvertedFavicon{PNG: append([]byte("png:"), original...), SizedPNG: map[int][]byte{}}
475 for _, s := range sizes {509 for _, s := range sizes {
476 out.SizedPNG[s] = append([]byte("png32:"), original...)510 out.SizedPNG[s] = append([]byte("png32:"), original...)
@@ -514,9 +548,15 @@ func (i *fakeInvalidators) InvalidateAll(_ context.Context, _ favicon.Key) error
514 return nil548 return nil
515 }549 }
516 550
551+type fakeFetchedIcon struct {
552+ bytes []byte
553+ contentType string
554+}
555+
517 type fakeFetcher struct {556 type fakeFetcher struct {
518- calls int557+ calls int
519- err error558+ err error
559+ responses map[string]fakeFetchedIcon
520 }560 }
521 561
522 func (f *fakeFetcher) Fetch(_ context.Context, req appport.FaviconFetchRequest) (*appport.FaviconFetchedIcon, error) {562 func (f *fakeFetcher) Fetch(_ context.Context, req appport.FaviconFetchRequest) (*appport.FaviconFetchedIcon, error) {
@@ -528,5 +568,9 @@ func (f *fakeFetcher) Fetch(_ context.Context, req appport.FaviconFetchRequest)
528 if iconURL == "" {568 if iconURL == "" {
529 iconURL = "https://example.com/favicon.ico"569 iconURL = "https://example.com/favicon.ico"
530 }570 }
531- return &appport.FaviconFetchedIcon{PageURL: req.PageURL, IconURL: iconURL, Bytes: []byte("fetched"), Source: favicon.SourceDuckDuckGo, ContentType: "image/png"}, nil571+ response := f.responses[iconURL]
572+ if response.bytes == nil {
573+ response = fakeFetchedIcon{bytes: []byte("fetched"), contentType: "image/png"}
574+ }
575+ return &appport.FaviconFetchedIcon{PageURL: req.PageURL, IconURL: iconURL, Bytes: response.bytes, Source: favicon.SourceDuckDuckGo, ContentType: response.contentType}, nil
532 }576 }
M internal/bootstrap/engine.go +1 -1
@@ -86,7 +86,7 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
8686 WindowlessFrameRateMax: cfg.Engine.CEF.CEFWindowlessFrameRateMax(),
8787 Input: cef.RuntimeInputConfig{
8888 ScrollWheelMultiplier: cfg.Engine.CEF.Input.ScrollWheelMultiplier,
89- ScrollTouchpadMultiplier: cfg.Engine.CEF.Input.ScrollTouchpadMultiplier,
89+ ScrollPreciseMultiplier: cfg.Engine.CEF.Input.ScrollPreciseMultiplier,
9090 ScrollHorizontalMultiplier: cfg.Engine.CEF.Input.ScrollHorizontalMultiplier,
9191 ScrollVerticalMultiplier: cfg.Engine.CEF.Input.ScrollVerticalMultiplier,
9292 ScrollMaxDelta: cfg.Engine.CEF.Input.ScrollMaxDelta,
@@ -86,7 +86,7 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
86 WindowlessFrameRateMax: cfg.Engine.CEF.CEFWindowlessFrameRateMax(),86 WindowlessFrameRateMax: cfg.Engine.CEF.CEFWindowlessFrameRateMax(),
87 Input: cef.RuntimeInputConfig{87 Input: cef.RuntimeInputConfig{
88 ScrollWheelMultiplier: cfg.Engine.CEF.Input.ScrollWheelMultiplier,88 ScrollWheelMultiplier: cfg.Engine.CEF.Input.ScrollWheelMultiplier,
89- ScrollTouchpadMultiplier: cfg.Engine.CEF.Input.ScrollTouchpadMultiplier,89+ ScrollPreciseMultiplier: cfg.Engine.CEF.Input.ScrollPreciseMultiplier,
90 ScrollHorizontalMultiplier: cfg.Engine.CEF.Input.ScrollHorizontalMultiplier,90 ScrollHorizontalMultiplier: cfg.Engine.CEF.Input.ScrollHorizontalMultiplier,
91 ScrollVerticalMultiplier: cfg.Engine.CEF.Input.ScrollVerticalMultiplier,91 ScrollVerticalMultiplier: cfg.Engine.CEF.Input.ScrollVerticalMultiplier,
92 ScrollMaxDelta: cfg.Engine.CEF.Input.ScrollMaxDelta,92 ScrollMaxDelta: cfg.Engine.CEF.Input.ScrollMaxDelta,
M internal/infrastructure/cef/runtime_config.go +1 -1
@@ -30,7 +30,7 @@ type RuntimeConfig struct {
3030
3131 type RuntimeInputConfig struct {
3232 ScrollWheelMultiplier float64
33- ScrollTouchpadMultiplier float64
33+ ScrollPreciseMultiplier float64
3434 ScrollHorizontalMultiplier float64
3535 ScrollVerticalMultiplier float64
3636 ScrollMaxDelta int32
@@ -30,7 +30,7 @@ type RuntimeConfig struct {
30 30
31 type RuntimeInputConfig struct {31 type RuntimeInputConfig struct {
32 ScrollWheelMultiplier float6432 ScrollWheelMultiplier float64
33- ScrollTouchpadMultiplier float6433+ ScrollPreciseMultiplier float64
34 ScrollHorizontalMultiplier float6434 ScrollHorizontalMultiplier float64
35 ScrollVerticalMultiplier float6435 ScrollVerticalMultiplier float64
36 ScrollMaxDelta int3236 ScrollMaxDelta int32
M internal/infrastructure/cef/webview.go +82 -93
@@ -225,8 +225,16 @@ type WebView struct {
225225 windowlessFrameRate int32
226226 backgroundColor uint32
227227 inputConfig RuntimeInputConfig
228- touchpadSwipeDX float64
229- touchpadSwipeDY float64
228+
229+ // Touchpad input diagnostics are debounced to avoid flooding logs while
230+ // debugging continuous scroll streams.
231+ inputDiagMu sync.Mutex
232+ inputDiagLastLog time.Time
233+ inputDiagEvents int
234+ inputDiagDX float64
235+ inputDiagDY float64
236+ inputDiagDeltaXSum int64
237+ inputDiagDeltaYSum int64
230238
231239 // Audio output factory and active stream.
232240 audioOutputFactory port.AudioOutputFactory
@@ -1462,7 +1470,7 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
14621470 Scale: 0,
14631471 Scroll: cef2gtk.ScrollOptions{
14641472 WheelMultiplier: wv.inputConfig.ScrollWheelMultiplier,
1465- TouchpadMultiplier: wv.inputConfig.ScrollTouchpadMultiplier,
1473+ PreciseMultiplier: wv.inputConfig.ScrollPreciseMultiplier,
14661474 HorizontalMultiplier: wv.inputConfig.ScrollHorizontalMultiplier,
14671475 VerticalMultiplier: wv.inputConfig.ScrollVerticalMultiplier,
14681476 MaxDelta: wv.inputConfig.ScrollMaxDelta,
@@ -1470,13 +1478,16 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
14701478 OnMiddleClick: func(_, _ float64) bool {
14711479 return wv.handleMiddleClickFromBridge()
14721480 },
1473- OnTouchpadSwipe: func(event cef2gtk.TouchpadSwipeEvent) cef2gtk.TouchpadSwipeDecision {
1474- if wv.handleTouchpadNavigationSwipe(event) {
1475- return cef2gtk.TouchpadSwipeConsume
1476- }
1477- return cef2gtk.TouchpadSwipePassthrough
1481+ OnScroll: wv.handleScrollInputDiagnostic,
1482+ NavigationSwipe: cef2gtk.NavigationSwipeOptions{
1483+ Enabled: wv.inputConfig.TouchpadNavigationEnabled,
1484+ MinDelta: wv.inputConfig.TouchpadNavigationMinDelta,
1485+ MaxVerticalRatio: wv.inputConfig.TouchpadNavigationMaxVerticalRatio,
14781486 },
1479- SelectionText: wv.selectedTextSnapshot,
1487+ CanNavigateBack: wv.CanGoBack,
1488+ CanNavigateForward: wv.CanGoForward,
1489+ OnNavigateSwipe: wv.handleNavigationSwipeAction,
1490+ SelectionText: wv.selectedTextSnapshot,
14801491 OnClipboardShortcut: func(action, text string) {
14811492 if wv.engine != nil {
14821493 wv.engine.handleExplicitClipboardBridgeText(wv.id, action, text)
@@ -1485,95 +1496,73 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
14851496 }
14861497 }
14871498
1488-type touchpadNavigationAction int
1489-
1490-const (
1491- touchpadNavigationNone touchpadNavigationAction = iota
1492- touchpadNavigationBack
1493- touchpadNavigationForward
1494-)
1495-
1496-func (wv *WebView) handleTouchpadNavigationSwipe(event cef2gtk.TouchpadSwipeEvent) bool {
1497- cfg := wv.inputConfig
1498- if !cfg.TouchpadNavigationEnabled || event.Fingers != 2 {
1499- wv.resetTouchpadNavigationSwipe()
1500- return false
1499+func (wv *WebView) handleScrollInputDiagnostic(event cef2gtk.ScrollEvent) cef2gtk.ScrollDecision {
1500+ if event.Phase != cef2gtk.ScrollPhaseUpdate || math.Abs(event.DX) == 0 {
1501+ return cef2gtk.ScrollForwardToCEF
15011502 }
15021503
1503- switch event.Phase {
1504- case cef2gtk.TouchpadGesturePhaseBegin:
1505- wv.resetTouchpadNavigationSwipe()
1506- return false
1507- case cef2gtk.TouchpadGesturePhaseUpdate:
1508- wv.accumulateTouchpadNavigationSwipe(event.DX, event.DY)
1509- return false
1510- case cef2gtk.TouchpadGesturePhaseEnd:
1511- dx, dy := wv.finishTouchpadNavigationSwipe()
1512- switch chooseTouchpadNavigationAction(cfg, dx, dy, wv.CanGoBack(), wv.CanGoForward()) {
1513- case touchpadNavigationBack:
1514- if err := wv.GoBack(wv.ctx); err != nil {
1515- logging.FromContext(wv.ctx).Warn().Err(err).Msg("touchpad back navigation failed")
1516- return false
1517- }
1518- return true
1519- case touchpadNavigationForward:
1520- if err := wv.GoForward(wv.ctx); err != nil {
1521- logging.FromContext(wv.ctx).Warn().Err(err).Msg("touchpad forward navigation failed")
1522- return false
1523- }
1524- return true
1525- default:
1526- return false
1527- }
1528- case cef2gtk.TouchpadGesturePhaseCancel:
1529- wv.resetTouchpadNavigationSwipe()
1530- return false
1531- default:
1532- return false
1504+ wv.inputDiagMu.Lock()
1505+ wv.inputDiagEvents++
1506+ wv.inputDiagDX += event.DX
1507+ wv.inputDiagDY += event.DY
1508+ wv.inputDiagDeltaXSum += int64(event.DeltaX)
1509+ wv.inputDiagDeltaYSum += int64(event.DeltaY)
1510+ now := time.Now()
1511+ if !wv.inputDiagLastLog.IsZero() && now.Sub(wv.inputDiagLastLog) < time.Second {
1512+ wv.inputDiagMu.Unlock()
1513+ return cef2gtk.ScrollForwardToCEF
1514+ }
1515+ count := wv.inputDiagEvents
1516+ dx, dy := wv.inputDiagDX, wv.inputDiagDY
1517+ deltaX, deltaY := wv.inputDiagDeltaXSum, wv.inputDiagDeltaYSum
1518+ wv.inputDiagEvents = 0
1519+ wv.inputDiagDX = 0
1520+ wv.inputDiagDY = 0
1521+ wv.inputDiagDeltaXSum = 0
1522+ wv.inputDiagDeltaYSum = 0
1523+ wv.inputDiagLastLog = now
1524+ wv.inputDiagMu.Unlock()
1525+
1526+ logging.FromContext(wv.ctx).Trace().
1527+ Uint64("webview_id", uint64(wv.id)).
1528+ Int("events", count).
1529+ Str("phase", fmt.Sprint(event.Phase)).
1530+ Str("unit", fmt.Sprint(event.Unit)).
1531+ Bool("unit_known", event.UnitKnown).
1532+ Float64("dx_sum", dx).
1533+ Float64("dy_sum", dy).
1534+ Int64("cef_delta_x_sum", deltaX).
1535+ Int64("cef_delta_y_sum", deltaY).
1536+ Bool("nav_enabled", wv.inputConfig.TouchpadNavigationEnabled).
1537+ Float64("nav_min_delta", wv.inputConfig.TouchpadNavigationMinDelta).
1538+ Float64("nav_max_vertical_ratio", wv.inputConfig.TouchpadNavigationMaxVerticalRatio).
1539+ Bool("can_go_back", wv.CanGoBack()).
1540+ Bool("can_go_forward", wv.CanGoForward()).
1541+ Msg("cef: touchpad horizontal scroll diagnostic")
1542+
1543+ return cef2gtk.ScrollForwardToCEF
1544+}
1545+
1546+func (wv *WebView) handleNavigationSwipeAction(action cef2gtk.NavigationSwipeAction) {
1547+ if wv.ctx != nil {
1548+ logging.FromContext(wv.ctx).Debug().
1549+ Uint64("webview_id", uint64(wv.id)).
1550+ Str("action", fmt.Sprint(action)).
1551+ Bool("can_go_back", wv.CanGoBack()).
1552+ Bool("can_go_forward", wv.CanGoForward()).
1553+ Msg("cef: touchpad navigation swipe recognized")
15331554 }
1534-}
1535-
1536-func (wv *WebView) resetTouchpadNavigationSwipe() {
1537- wv.mu.Lock()
1538- wv.touchpadSwipeDX = 0
1539- wv.touchpadSwipeDY = 0
1540- wv.mu.Unlock()
1541-}
15421555
1543-func (wv *WebView) accumulateTouchpadNavigationSwipe(dx, dy float64) {
1544- wv.mu.Lock()
1545- wv.touchpadSwipeDX += dx
1546- wv.touchpadSwipeDY += dy
1547- wv.mu.Unlock()
1548-}
1549-
1550-func (wv *WebView) finishTouchpadNavigationSwipe() (float64, float64) {
1551- wv.mu.Lock()
1552- defer wv.mu.Unlock()
1553- dx, dy := wv.touchpadSwipeDX, wv.touchpadSwipeDY
1554- wv.touchpadSwipeDX = 0
1555- wv.touchpadSwipeDY = 0
1556- return dx, dy
1557-}
1558-
1559-func chooseTouchpadNavigationAction(cfg RuntimeInputConfig, dx, dy float64, canGoBack, canGoForward bool) touchpadNavigationAction {
1560- if !cfg.TouchpadNavigationEnabled {
1561- return touchpadNavigationNone
1562- }
1563- absDX, absDY := math.Abs(dx), math.Abs(dy)
1564- if absDX < cfg.TouchpadNavigationMinDelta {
1565- return touchpadNavigationNone
1566- }
1567- if absDY > absDX*cfg.TouchpadNavigationMaxVerticalRatio {
1568- return touchpadNavigationNone
1569- }
1570- if dx > 0 && canGoBack {
1571- return touchpadNavigationBack
1572- }
1573- if dx < 0 && canGoForward {
1574- return touchpadNavigationForward
1556+ switch action {
1557+ case cef2gtk.NavigationSwipeBack:
1558+ if err := wv.GoBack(wv.ctx); err != nil && wv.ctx != nil {
1559+ logging.FromContext(wv.ctx).Warn().Err(err).Msg("touchpad back navigation failed")
1560+ }
1561+ case cef2gtk.NavigationSwipeForward:
1562+ if err := wv.GoForward(wv.ctx); err != nil && wv.ctx != nil {
1563+ logging.FromContext(wv.ctx).Warn().Err(err).Msg("touchpad forward navigation failed")
1564+ }
15751565 }
1576- return touchpadNavigationNone
15771566 }
15781567
15791568 func (wv *WebView) viewBridgeScale() float64 {
@@ -225,8 +225,16 @@ type WebView struct {
225 windowlessFrameRate int32225 windowlessFrameRate int32
226 backgroundColor uint32226 backgroundColor uint32
227 inputConfig RuntimeInputConfig227 inputConfig RuntimeInputConfig
228- touchpadSwipeDX float64228+
229- touchpadSwipeDY float64229+ // Touchpad input diagnostics are debounced to avoid flooding logs while
230+ // debugging continuous scroll streams.
231+ inputDiagMu sync.Mutex
232+ inputDiagLastLog time.Time
233+ inputDiagEvents int
234+ inputDiagDX float64
235+ inputDiagDY float64
236+ inputDiagDeltaXSum int64
237+ inputDiagDeltaYSum int64
230 238
231 // Audio output factory and active stream.239 // Audio output factory and active stream.
232 audioOutputFactory port.AudioOutputFactory240 audioOutputFactory port.AudioOutputFactory
@@ -1462,7 +1470,7 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
1462 Scale: 0,1470 Scale: 0,
1463 Scroll: cef2gtk.ScrollOptions{1471 Scroll: cef2gtk.ScrollOptions{
1464 WheelMultiplier: wv.inputConfig.ScrollWheelMultiplier,1472 WheelMultiplier: wv.inputConfig.ScrollWheelMultiplier,
1465- TouchpadMultiplier: wv.inputConfig.ScrollTouchpadMultiplier,1473+ PreciseMultiplier: wv.inputConfig.ScrollPreciseMultiplier,
1466 HorizontalMultiplier: wv.inputConfig.ScrollHorizontalMultiplier,1474 HorizontalMultiplier: wv.inputConfig.ScrollHorizontalMultiplier,
1467 VerticalMultiplier: wv.inputConfig.ScrollVerticalMultiplier,1475 VerticalMultiplier: wv.inputConfig.ScrollVerticalMultiplier,
1468 MaxDelta: wv.inputConfig.ScrollMaxDelta,1476 MaxDelta: wv.inputConfig.ScrollMaxDelta,
@@ -1470,13 +1478,16 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
1470 OnMiddleClick: func(_, _ float64) bool {1478 OnMiddleClick: func(_, _ float64) bool {
1471 return wv.handleMiddleClickFromBridge()1479 return wv.handleMiddleClickFromBridge()
1472 },1480 },
1473- OnTouchpadSwipe: func(event cef2gtk.TouchpadSwipeEvent) cef2gtk.TouchpadSwipeDecision {1481+ OnScroll: wv.handleScrollInputDiagnostic,
1474- if wv.handleTouchpadNavigationSwipe(event) {1482+ NavigationSwipe: cef2gtk.NavigationSwipeOptions{
1475- return cef2gtk.TouchpadSwipeConsume1483+ Enabled: wv.inputConfig.TouchpadNavigationEnabled,
1476- }1484+ MinDelta: wv.inputConfig.TouchpadNavigationMinDelta,
1477- return cef2gtk.TouchpadSwipePassthrough1485+ MaxVerticalRatio: wv.inputConfig.TouchpadNavigationMaxVerticalRatio,
1478 },1486 },
1479- SelectionText: wv.selectedTextSnapshot,1487+ CanNavigateBack: wv.CanGoBack,
1488+ CanNavigateForward: wv.CanGoForward,
1489+ OnNavigateSwipe: wv.handleNavigationSwipeAction,
1490+ SelectionText: wv.selectedTextSnapshot,
1480 OnClipboardShortcut: func(action, text string) {1491 OnClipboardShortcut: func(action, text string) {
1481 if wv.engine != nil {1492 if wv.engine != nil {
1482 wv.engine.handleExplicitClipboardBridgeText(wv.id, action, text)1493 wv.engine.handleExplicitClipboardBridgeText(wv.id, action, text)
@@ -1485,95 +1496,73 @@ func (wv *WebView) bridgeInputOptions() cef2gtk.InputOptions {
1485 }1496 }
1486 }1497 }
1487 1498
1488-type touchpadNavigationAction int1499+func (wv *WebView) handleScrollInputDiagnostic(event cef2gtk.ScrollEvent) cef2gtk.ScrollDecision {
1489-1500+ if event.Phase != cef2gtk.ScrollPhaseUpdate || math.Abs(event.DX) == 0 {
1490-const (1501+ return cef2gtk.ScrollForwardToCEF
1491- touchpadNavigationNone touchpadNavigationAction = iota
1492- touchpadNavigationBack
1493- touchpadNavigationForward
1494-)
1495-
1496-func (wv *WebView) handleTouchpadNavigationSwipe(event cef2gtk.TouchpadSwipeEvent) bool {
1497- cfg := wv.inputConfig
1498- if !cfg.TouchpadNavigationEnabled || event.Fingers != 2 {
1499- wv.resetTouchpadNavigationSwipe()
1500- return false
1501 }1502 }
1502 1503
1503- switch event.Phase {1504+ wv.inputDiagMu.Lock()
1504- case cef2gtk.TouchpadGesturePhaseBegin:1505+ wv.inputDiagEvents++
1505- wv.resetTouchpadNavigationSwipe()1506+ wv.inputDiagDX += event.DX
1506- return false1507+ wv.inputDiagDY += event.DY
1507- case cef2gtk.TouchpadGesturePhaseUpdate:1508+ wv.inputDiagDeltaXSum += int64(event.DeltaX)
1508- wv.accumulateTouchpadNavigationSwipe(event.DX, event.DY)1509+ wv.inputDiagDeltaYSum += int64(event.DeltaY)
1509- return false1510+ now := time.Now()
1510- case cef2gtk.TouchpadGesturePhaseEnd:1511+ if !wv.inputDiagLastLog.IsZero() && now.Sub(wv.inputDiagLastLog) < time.Second {
1511- dx, dy := wv.finishTouchpadNavigationSwipe()1512+ wv.inputDiagMu.Unlock()
1512- switch chooseTouchpadNavigationAction(cfg, dx, dy, wv.CanGoBack(), wv.CanGoForward()) {1513+ return cef2gtk.ScrollForwardToCEF
1513- case touchpadNavigationBack:1514+ }
1514- if err := wv.GoBack(wv.ctx); err != nil {1515+ count := wv.inputDiagEvents
1515- logging.FromContext(wv.ctx).Warn().Err(err).Msg("touchpad back navigation failed")1516+ dx, dy := wv.inputDiagDX, wv.inputDiagDY
1516- return false1517+ deltaX, deltaY := wv.inputDiagDeltaXSum, wv.inputDiagDeltaYSum
1517- }1518+ wv.inputDiagEvents = 0
1518- return true1519+ wv.inputDiagDX = 0
1519- case touchpadNavigationForward:1520+ wv.inputDiagDY = 0
1520- if err := wv.GoForward(wv.ctx); err != nil {1521+ wv.inputDiagDeltaXSum = 0
1521- logging.FromContext(wv.ctx).Warn().Err(err).Msg("touchpad forward navigation failed")1522+ wv.inputDiagDeltaYSum = 0
1522- return false1523+ wv.inputDiagLastLog = now
1523- }1524+ wv.inputDiagMu.Unlock()
1524- return true1525+
1525- default:1526+ logging.FromContext(wv.ctx).Trace().
1526- return false1527+ Uint64("webview_id", uint64(wv.id)).
1527- }1528+ Int("events", count).
1528- case cef2gtk.TouchpadGesturePhaseCancel:1529+ Str("phase", fmt.Sprint(event.Phase)).
1529- wv.resetTouchpadNavigationSwipe()1530+ Str("unit", fmt.Sprint(event.Unit)).
1530- return false1531+ Bool("unit_known", event.UnitKnown).
1531- default:1532+ Float64("dx_sum", dx).
1532- return false1533+ Float64("dy_sum", dy).
1534+ Int64("cef_delta_x_sum", deltaX).
1535+ Int64("cef_delta_y_sum", deltaY).
1536+ Bool("nav_enabled", wv.inputConfig.TouchpadNavigationEnabled).
1537+ Float64("nav_min_delta", wv.inputConfig.TouchpadNavigationMinDelta).
1538+ Float64("nav_max_vertical_ratio", wv.inputConfig.TouchpadNavigationMaxVerticalRatio).
1539+ Bool("can_go_back", wv.CanGoBack()).
1540+ Bool("can_go_forward", wv.CanGoForward()).
1541+ Msg("cef: touchpad horizontal scroll diagnostic")
1542+
1543+ return cef2gtk.ScrollForwardToCEF
1544+}
1545+
1546+func (wv *WebView) handleNavigationSwipeAction(action cef2gtk.NavigationSwipeAction) {
1547+ if wv.ctx != nil {
1548+ logging.FromContext(wv.ctx).Debug().
1549+ Uint64("webview_id", uint64(wv.id)).
1550+ Str("action", fmt.Sprint(action)).
1551+ Bool("can_go_back", wv.CanGoBack()).
1552+ Bool("can_go_forward", wv.CanGoForward()).
1553+ Msg("cef: touchpad navigation swipe recognized")
1533 }1554 }
1534-}
1535-
1536-func (wv *WebView) resetTouchpadNavigationSwipe() {
1537- wv.mu.Lock()
1538- wv.touchpadSwipeDX = 0
1539- wv.touchpadSwipeDY = 0
1540- wv.mu.Unlock()
1541-}
1542 1555
1543-func (wv *WebView) accumulateTouchpadNavigationSwipe(dx, dy float64) {1556+ switch action {
1544- wv.mu.Lock()1557+ case cef2gtk.NavigationSwipeBack:
1545- wv.touchpadSwipeDX += dx1558+ if err := wv.GoBack(wv.ctx); err != nil && wv.ctx != nil {
1546- wv.touchpadSwipeDY += dy1559+ logging.FromContext(wv.ctx).Warn().Err(err).Msg("touchpad back navigation failed")
1547- wv.mu.Unlock()1560+ }
1548-}1561+ case cef2gtk.NavigationSwipeForward:
1549-1562+ if err := wv.GoForward(wv.ctx); err != nil && wv.ctx != nil {
1550-func (wv *WebView) finishTouchpadNavigationSwipe() (float64, float64) {1563+ logging.FromContext(wv.ctx).Warn().Err(err).Msg("touchpad forward navigation failed")
1551- wv.mu.Lock()1564+ }
1552- defer wv.mu.Unlock()
1553- dx, dy := wv.touchpadSwipeDX, wv.touchpadSwipeDY
1554- wv.touchpadSwipeDX = 0
1555- wv.touchpadSwipeDY = 0
1556- return dx, dy
1557-}
1558-
1559-func chooseTouchpadNavigationAction(cfg RuntimeInputConfig, dx, dy float64, canGoBack, canGoForward bool) touchpadNavigationAction {
1560- if !cfg.TouchpadNavigationEnabled {
1561- return touchpadNavigationNone
1562- }
1563- absDX, absDY := math.Abs(dx), math.Abs(dy)
1564- if absDX < cfg.TouchpadNavigationMinDelta {
1565- return touchpadNavigationNone
1566- }
1567- if absDY > absDX*cfg.TouchpadNavigationMaxVerticalRatio {
1568- return touchpadNavigationNone
1569- }
1570- if dx > 0 && canGoBack {
1571- return touchpadNavigationBack
1572- }
1573- if dx < 0 && canGoForward {
1574- return touchpadNavigationForward
1575 }1565 }
1576- return touchpadNavigationNone
1577 }1566 }
1578 1567
1579 func (wv *WebView) viewBridgeScale() float64 {1568 func (wv *WebView) viewBridgeScale() float64 {
M internal/infrastructure/cef/webview_input_test.go +9 -40
@@ -13,7 +13,7 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T)
1313 nativeWidget: nativeWidget,
1414 inputConfig: RuntimeInputConfig{
1515 ScrollWheelMultiplier: 1.25,
16- ScrollTouchpadMultiplier: 0.35,
16+ ScrollPreciseMultiplier: 2.5,
1717 ScrollHorizontalMultiplier: 0.75,
1818 ScrollVerticalMultiplier: 1.5,
1919 ScrollMaxDelta: 120,
@@ -27,50 +27,19 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T)
2727
2828 require.Zero(t, opts.Scale)
2929 require.InDelta(t, 1.25, opts.Scroll.WheelMultiplier, 0.001)
30- require.InDelta(t, 0.35, opts.Scroll.TouchpadMultiplier, 0.001)
30+ require.InDelta(t, 2.5, opts.Scroll.PreciseMultiplier, 0.001)
3131 require.InDelta(t, 0.75, opts.Scroll.HorizontalMultiplier, 0.001)
3232 require.InDelta(t, 1.5, opts.Scroll.VerticalMultiplier, 0.001)
3333 require.Equal(t, int32(120), opts.Scroll.MaxDelta)
3434 require.NotNil(t, opts.OnMiddleClick)
35- require.NotNil(t, opts.OnTouchpadSwipe)
35+ require.NotNil(t, opts.OnScroll)
36+ require.True(t, opts.NavigationSwipe.Enabled)
37+ require.InDelta(t, 80.0, opts.NavigationSwipe.MinDelta, 0.001)
38+ require.InDelta(t, 0.5, opts.NavigationSwipe.MaxVerticalRatio, 0.001)
39+ require.NotNil(t, opts.CanNavigateBack)
40+ require.NotNil(t, opts.CanNavigateForward)
41+ require.NotNil(t, opts.OnNavigateSwipe)
3642 require.NotNil(t, opts.SelectionText)
3743 require.NotNil(t, opts.OnClipboardShortcut)
3844 require.Same(t, nativeWidget, wv.nativeWidget)
3945 }
40-
41-func TestChooseTouchpadNavigationAction(t *testing.T) {
42- cfg := RuntimeInputConfig{
43- TouchpadNavigationEnabled: true,
44- TouchpadNavigationMinDelta: 80,
45- TouchpadNavigationMaxVerticalRatio: 0.5,
46- }
47-
48- tests := []struct {
49- name string
50- cfg *RuntimeInputConfig
51- dx, dy float64
52- canGoBack bool
53- canGoForward bool
54- want touchpadNavigationAction
55- }{
56- {name: "right swipe goes back", dx: 100, dy: 10, canGoBack: true, want: touchpadNavigationBack},
57- {name: "left swipe goes forward", dx: -100, dy: 10, canGoForward: true, want: touchpadNavigationForward},
58- {name: "below threshold ignored", dx: 40, dy: 0, canGoBack: true, want: touchpadNavigationNone},
59- {name: "disabled ignored", cfg: &RuntimeInputConfig{TouchpadNavigationEnabled: false, TouchpadNavigationMinDelta: 80, TouchpadNavigationMaxVerticalRatio: 0.5}, dx: 100, dy: 0, canGoBack: true, want: touchpadNavigationNone},
60- {name: "exact threshold goes back", dx: 80, dy: 0, canGoBack: true, want: touchpadNavigationBack},
61- {name: "vertical diagonal ignored", dx: 100, dy: 80, canGoBack: true, want: touchpadNavigationNone},
62- {name: "unavailable back ignored", dx: 100, dy: 0, canGoBack: false, want: touchpadNavigationNone},
63- {name: "unavailable forward ignored", dx: -100, dy: 0, canGoForward: false, want: touchpadNavigationNone},
64- }
65-
66- for _, tt := range tests {
67- t.Run(tt.name, func(t *testing.T) {
68- testCfg := cfg
69- if tt.cfg != nil {
70- testCfg = *tt.cfg
71- }
72- got := chooseTouchpadNavigationAction(testCfg, tt.dx, tt.dy, tt.canGoBack, tt.canGoForward)
73- require.Equal(t, tt.want, got)
74- })
75- }
76-}
@@ -13,7 +13,7 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T)
13 nativeWidget: nativeWidget,13 nativeWidget: nativeWidget,
14 inputConfig: RuntimeInputConfig{14 inputConfig: RuntimeInputConfig{
15 ScrollWheelMultiplier: 1.25,15 ScrollWheelMultiplier: 1.25,
16- ScrollTouchpadMultiplier: 0.35,16+ ScrollPreciseMultiplier: 2.5,
17 ScrollHorizontalMultiplier: 0.75,17 ScrollHorizontalMultiplier: 0.75,
18 ScrollVerticalMultiplier: 1.5,18 ScrollVerticalMultiplier: 1.5,
19 ScrollMaxDelta: 120,19 ScrollMaxDelta: 120,
@@ -27,50 +27,19 @@ func TestWebViewBridgeInputOptions_LeavesTargetSelectionToAdapter(t *testing.T)
27 27
28 require.Zero(t, opts.Scale)28 require.Zero(t, opts.Scale)
29 require.InDelta(t, 1.25, opts.Scroll.WheelMultiplier, 0.001)29 require.InDelta(t, 1.25, opts.Scroll.WheelMultiplier, 0.001)
30- require.InDelta(t, 0.35, opts.Scroll.TouchpadMultiplier, 0.001)30+ require.InDelta(t, 2.5, opts.Scroll.PreciseMultiplier, 0.001)
31 require.InDelta(t, 0.75, opts.Scroll.HorizontalMultiplier, 0.001)31 require.InDelta(t, 0.75, opts.Scroll.HorizontalMultiplier, 0.001)
32 require.InDelta(t, 1.5, opts.Scroll.VerticalMultiplier, 0.001)32 require.InDelta(t, 1.5, opts.Scroll.VerticalMultiplier, 0.001)
33 require.Equal(t, int32(120), opts.Scroll.MaxDelta)33 require.Equal(t, int32(120), opts.Scroll.MaxDelta)
34 require.NotNil(t, opts.OnMiddleClick)34 require.NotNil(t, opts.OnMiddleClick)
35- require.NotNil(t, opts.OnTouchpadSwipe)35+ require.NotNil(t, opts.OnScroll)
36+ require.True(t, opts.NavigationSwipe.Enabled)
37+ require.InDelta(t, 80.0, opts.NavigationSwipe.MinDelta, 0.001)
38+ require.InDelta(t, 0.5, opts.NavigationSwipe.MaxVerticalRatio, 0.001)
39+ require.NotNil(t, opts.CanNavigateBack)
40+ require.NotNil(t, opts.CanNavigateForward)
41+ require.NotNil(t, opts.OnNavigateSwipe)
36 require.NotNil(t, opts.SelectionText)42 require.NotNil(t, opts.SelectionText)
37 require.NotNil(t, opts.OnClipboardShortcut)43 require.NotNil(t, opts.OnClipboardShortcut)
38 require.Same(t, nativeWidget, wv.nativeWidget)44 require.Same(t, nativeWidget, wv.nativeWidget)
39 }45 }
40-
41-func TestChooseTouchpadNavigationAction(t *testing.T) {
42- cfg := RuntimeInputConfig{
43- TouchpadNavigationEnabled: true,
44- TouchpadNavigationMinDelta: 80,
45- TouchpadNavigationMaxVerticalRatio: 0.5,
46- }
47-
48- tests := []struct {
49- name string
50- cfg *RuntimeInputConfig
51- dx, dy float64
52- canGoBack bool
53- canGoForward bool
54- want touchpadNavigationAction
55- }{
56- {name: "right swipe goes back", dx: 100, dy: 10, canGoBack: true, want: touchpadNavigationBack},
57- {name: "left swipe goes forward", dx: -100, dy: 10, canGoForward: true, want: touchpadNavigationForward},
58- {name: "below threshold ignored", dx: 40, dy: 0, canGoBack: true, want: touchpadNavigationNone},
59- {name: "disabled ignored", cfg: &RuntimeInputConfig{TouchpadNavigationEnabled: false, TouchpadNavigationMinDelta: 80, TouchpadNavigationMaxVerticalRatio: 0.5}, dx: 100, dy: 0, canGoBack: true, want: touchpadNavigationNone},
60- {name: "exact threshold goes back", dx: 80, dy: 0, canGoBack: true, want: touchpadNavigationBack},
61- {name: "vertical diagonal ignored", dx: 100, dy: 80, canGoBack: true, want: touchpadNavigationNone},
62- {name: "unavailable back ignored", dx: 100, dy: 0, canGoBack: false, want: touchpadNavigationNone},
63- {name: "unavailable forward ignored", dx: -100, dy: 0, canGoForward: false, want: touchpadNavigationNone},
64- }
65-
66- for _, tt := range tests {
67- t.Run(tt.name, func(t *testing.T) {
68- testCfg := cfg
69- if tt.cfg != nil {
70- testCfg = *tt.cfg
71- }
72- got := chooseTouchpadNavigationAction(testCfg, tt.dx, tt.dy, tt.canGoBack, tt.canGoForward)
73- require.Equal(t, tt.want, got)
74- })
75- }
76-}
M internal/infrastructure/config/defaults.go +11 -11
@@ -67,16 +67,16 @@ const (
6767 defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%)
6868
6969 // Performance defaults
70- defaultZoomCacheSize = 256 // domains to cache (~20KB memory)
71- defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup
72- defaultCEFWindowlessFrameRate = 60 // static OSR frame rate when adaptive CEF pacing is disabled
73- defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap
74- defaultCEFScrollMultiplier = 1.0
75- defaultCEFScrollTouchpadMultiplier = 0.35
76- defaultCEFScrollMaxDelta = 0
77- defaultCEFTouchpadNavigation = true
78- defaultCEFTouchpadNavigationDelta = 80.0
79- defaultCEFTouchpadNavigationRatio = 0.5
70+ defaultZoomCacheSize = 256 // domains to cache (~20KB memory)
71+ defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup
72+ defaultCEFWindowlessFrameRate = 60 // static OSR frame rate when adaptive CEF pacing is disabled
73+ defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap
74+ defaultCEFScrollMultiplier = 1.0
75+ defaultCEFScrollPreciseMultiplier = 2.5
76+ defaultCEFScrollMaxDelta = 0
77+ defaultCEFTouchpadNavigation = true
78+ defaultCEFTouchpadNavigationDelta = 200.0
79+ defaultCEFTouchpadNavigationRatio = 0.5
8080
8181 // Skia threading defaults (0 = unset, -1 = unset for GPU threads)
8282 defaultSkiaCPUPaintingThreads = 0
@@ -186,7 +186,7 @@ func DefaultConfig() *Config {
186186 EnableAudioHandler: true,
187187 Input: CEFInputConfig{
188188 ScrollWheelMultiplier: defaultCEFScrollMultiplier,
189- ScrollTouchpadMultiplier: defaultCEFScrollTouchpadMultiplier,
189+ ScrollPreciseMultiplier: defaultCEFScrollPreciseMultiplier,
190190 ScrollHorizontalMultiplier: defaultCEFScrollMultiplier,
191191 ScrollVerticalMultiplier: defaultCEFScrollMultiplier,
192192 ScrollMaxDelta: defaultCEFScrollMaxDelta,
@@ -67,16 +67,16 @@ const (
67 defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%)67 defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%)
68 68
69 // Performance defaults69 // Performance defaults
70- defaultZoomCacheSize = 256 // domains to cache (~20KB memory)70+ defaultZoomCacheSize = 256 // domains to cache (~20KB memory)
71- defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup71+ defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup
72- defaultCEFWindowlessFrameRate = 60 // static OSR frame rate when adaptive CEF pacing is disabled72+ defaultCEFWindowlessFrameRate = 60 // static OSR frame rate when adaptive CEF pacing is disabled
73- defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap73+ defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap
74- defaultCEFScrollMultiplier = 1.074+ defaultCEFScrollMultiplier = 1.0
75- defaultCEFScrollTouchpadMultiplier = 0.3575+ defaultCEFScrollPreciseMultiplier = 2.5
76- defaultCEFScrollMaxDelta = 076+ defaultCEFScrollMaxDelta = 0
77- defaultCEFTouchpadNavigation = true77+ defaultCEFTouchpadNavigation = true
78- defaultCEFTouchpadNavigationDelta = 80.078+ defaultCEFTouchpadNavigationDelta = 200.0
79- defaultCEFTouchpadNavigationRatio = 0.579+ defaultCEFTouchpadNavigationRatio = 0.5
80 80
81 // Skia threading defaults (0 = unset, -1 = unset for GPU threads)81 // Skia threading defaults (0 = unset, -1 = unset for GPU threads)
82 defaultSkiaCPUPaintingThreads = 082 defaultSkiaCPUPaintingThreads = 0
@@ -186,7 +186,7 @@ func DefaultConfig() *Config {
186 EnableAudioHandler: true,186 EnableAudioHandler: true,
187 Input: CEFInputConfig{187 Input: CEFInputConfig{
188 ScrollWheelMultiplier: defaultCEFScrollMultiplier,188 ScrollWheelMultiplier: defaultCEFScrollMultiplier,
189- ScrollTouchpadMultiplier: defaultCEFScrollTouchpadMultiplier,189+ ScrollPreciseMultiplier: defaultCEFScrollPreciseMultiplier,
190 ScrollHorizontalMultiplier: defaultCEFScrollMultiplier,190 ScrollHorizontalMultiplier: defaultCEFScrollMultiplier,
191 ScrollVerticalMultiplier: defaultCEFScrollMultiplier,191 ScrollVerticalMultiplier: defaultCEFScrollMultiplier,
192 ScrollMaxDelta: defaultCEFScrollMaxDelta,192 ScrollMaxDelta: defaultCEFScrollMaxDelta,
M internal/infrastructure/config/defaults_test.go +1 -1
@@ -26,7 +26,7 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) {
2626 assert.True(t, cfg.Engine.CEF.EnableAudioHandler)
2727 assert.False(t, cfg.Engine.CEF.TraceHandlers)
2828 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollWheelMultiplier, 0.001)
29- assert.InDelta(t, defaultCEFScrollTouchpadMultiplier, cfg.Engine.CEF.Input.ScrollTouchpadMultiplier, 0.001)
29+ assert.InDelta(t, defaultCEFScrollPreciseMultiplier, cfg.Engine.CEF.Input.ScrollPreciseMultiplier, 0.001)
3030 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollHorizontalMultiplier, 0.001)
3131 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollVerticalMultiplier, 0.001)
3232 assert.Equal(t, int32(defaultCEFScrollMaxDelta), cfg.Engine.CEF.Input.ScrollMaxDelta)
@@ -26,7 +26,7 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) {
26 assert.True(t, cfg.Engine.CEF.EnableAudioHandler)26 assert.True(t, cfg.Engine.CEF.EnableAudioHandler)
27 assert.False(t, cfg.Engine.CEF.TraceHandlers)27 assert.False(t, cfg.Engine.CEF.TraceHandlers)
28 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollWheelMultiplier, 0.001)28 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollWheelMultiplier, 0.001)
29- assert.InDelta(t, defaultCEFScrollTouchpadMultiplier, cfg.Engine.CEF.Input.ScrollTouchpadMultiplier, 0.001)29+ assert.InDelta(t, defaultCEFScrollPreciseMultiplier, cfg.Engine.CEF.Input.ScrollPreciseMultiplier, 0.001)
30 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollHorizontalMultiplier, 0.001)30 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollHorizontalMultiplier, 0.001)
31 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollVerticalMultiplier, 0.001)31 assert.InDelta(t, defaultCEFScrollMultiplier, cfg.Engine.CEF.Input.ScrollVerticalMultiplier, 0.001)
32 assert.Equal(t, int32(defaultCEFScrollMaxDelta), cfg.Engine.CEF.Input.ScrollMaxDelta)32 assert.Equal(t, int32(defaultCEFScrollMaxDelta), cfg.Engine.CEF.Input.ScrollMaxDelta)
M internal/infrastructure/config/engine.go +1 -1
@@ -121,7 +121,7 @@ type CEFEngineConfig struct {
121121 // CEFInputConfig holds CEF-specific input translation options.
122122 type CEFInputConfig struct {
123123 ScrollWheelMultiplier float64 `mapstructure:"scroll_wheel_multiplier" toml:"scroll_wheel_multiplier" yaml:"scroll_wheel_multiplier"` //nolint:lll // struct tags exceed lll limit
124- ScrollTouchpadMultiplier float64 `mapstructure:"scroll_touchpad_multiplier" toml:"scroll_touchpad_multiplier" yaml:"scroll_touchpad_multiplier"` //nolint:lll // struct tags exceed lll limit
124+ ScrollPreciseMultiplier float64 `mapstructure:"scroll_precise_multiplier" toml:"scroll_precise_multiplier" yaml:"scroll_precise_multiplier"` //nolint:lll // struct tags exceed lll limit
125125 ScrollHorizontalMultiplier float64 `mapstructure:"scroll_horizontal_multiplier" toml:"scroll_horizontal_multiplier" yaml:"scroll_horizontal_multiplier"` //nolint:lll // struct tags exceed lll limit
126126 ScrollVerticalMultiplier float64 `mapstructure:"scroll_vertical_multiplier" toml:"scroll_vertical_multiplier" yaml:"scroll_vertical_multiplier"` //nolint:lll // struct tags exceed lll limit
127127 ScrollMaxDelta int32 `mapstructure:"scroll_max_delta" toml:"scroll_max_delta" yaml:"scroll_max_delta"` //nolint:lll // struct tags exceed lll limit
@@ -121,7 +121,7 @@ type CEFEngineConfig struct {
121 // CEFInputConfig holds CEF-specific input translation options.121 // CEFInputConfig holds CEF-specific input translation options.
122 type CEFInputConfig struct {122 type CEFInputConfig struct {
123 ScrollWheelMultiplier float64 `mapstructure:"scroll_wheel_multiplier" toml:"scroll_wheel_multiplier" yaml:"scroll_wheel_multiplier"` //nolint:lll // struct tags exceed lll limit123 ScrollWheelMultiplier float64 `mapstructure:"scroll_wheel_multiplier" toml:"scroll_wheel_multiplier" yaml:"scroll_wheel_multiplier"` //nolint:lll // struct tags exceed lll limit
124- ScrollTouchpadMultiplier float64 `mapstructure:"scroll_touchpad_multiplier" toml:"scroll_touchpad_multiplier" yaml:"scroll_touchpad_multiplier"` //nolint:lll // struct tags exceed lll limit124+ ScrollPreciseMultiplier float64 `mapstructure:"scroll_precise_multiplier" toml:"scroll_precise_multiplier" yaml:"scroll_precise_multiplier"` //nolint:lll // struct tags exceed lll limit
125 ScrollHorizontalMultiplier float64 `mapstructure:"scroll_horizontal_multiplier" toml:"scroll_horizontal_multiplier" yaml:"scroll_horizontal_multiplier"` //nolint:lll // struct tags exceed lll limit125 ScrollHorizontalMultiplier float64 `mapstructure:"scroll_horizontal_multiplier" toml:"scroll_horizontal_multiplier" yaml:"scroll_horizontal_multiplier"` //nolint:lll // struct tags exceed lll limit
126 ScrollVerticalMultiplier float64 `mapstructure:"scroll_vertical_multiplier" toml:"scroll_vertical_multiplier" yaml:"scroll_vertical_multiplier"` //nolint:lll // struct tags exceed lll limit126 ScrollVerticalMultiplier float64 `mapstructure:"scroll_vertical_multiplier" toml:"scroll_vertical_multiplier" yaml:"scroll_vertical_multiplier"` //nolint:lll // struct tags exceed lll limit
127 ScrollMaxDelta int32 `mapstructure:"scroll_max_delta" toml:"scroll_max_delta" yaml:"scroll_max_delta"` //nolint:lll // struct tags exceed lll limit127 ScrollMaxDelta int32 `mapstructure:"scroll_max_delta" toml:"scroll_max_delta" yaml:"scroll_max_delta"` //nolint:lll // struct tags exceed lll limit
M internal/infrastructure/config/loader.go +1 -1
@@ -783,7 +783,7 @@ func (m *Manager) setEngineDefaults(defaults *Config) {
783783
784784 func (m *Manager) setCEFInputDefaults(input CEFInputConfig) {
785785 m.viper.SetDefault("engine.cef.input.scroll_wheel_multiplier", input.ScrollWheelMultiplier)
786- m.viper.SetDefault("engine.cef.input.scroll_touchpad_multiplier", input.ScrollTouchpadMultiplier)
786+ m.viper.SetDefault("engine.cef.input.scroll_precise_multiplier", input.ScrollPreciseMultiplier)
787787 m.viper.SetDefault("engine.cef.input.scroll_horizontal_multiplier", input.ScrollHorizontalMultiplier)
788788 m.viper.SetDefault("engine.cef.input.scroll_vertical_multiplier", input.ScrollVerticalMultiplier)
789789 m.viper.SetDefault("engine.cef.input.scroll_max_delta", input.ScrollMaxDelta)
@@ -783,7 +783,7 @@ func (m *Manager) setEngineDefaults(defaults *Config) {
783 783
784 func (m *Manager) setCEFInputDefaults(input CEFInputConfig) {784 func (m *Manager) setCEFInputDefaults(input CEFInputConfig) {
785 m.viper.SetDefault("engine.cef.input.scroll_wheel_multiplier", input.ScrollWheelMultiplier)785 m.viper.SetDefault("engine.cef.input.scroll_wheel_multiplier", input.ScrollWheelMultiplier)
786- m.viper.SetDefault("engine.cef.input.scroll_touchpad_multiplier", input.ScrollTouchpadMultiplier)786+ m.viper.SetDefault("engine.cef.input.scroll_precise_multiplier", input.ScrollPreciseMultiplier)
787 m.viper.SetDefault("engine.cef.input.scroll_horizontal_multiplier", input.ScrollHorizontalMultiplier)787 m.viper.SetDefault("engine.cef.input.scroll_horizontal_multiplier", input.ScrollHorizontalMultiplier)
788 m.viper.SetDefault("engine.cef.input.scroll_vertical_multiplier", input.ScrollVerticalMultiplier)788 m.viper.SetDefault("engine.cef.input.scroll_vertical_multiplier", input.ScrollVerticalMultiplier)
789 m.viper.SetDefault("engine.cef.input.scroll_max_delta", input.ScrollMaxDelta)789 m.viper.SetDefault("engine.cef.input.scroll_max_delta", input.ScrollMaxDelta)
M internal/infrastructure/config/loader_test.go +1 -1
@@ -18,7 +18,7 @@ func TestSetEngineDefaults(t *testing.T) {
1818 assert.Equal(t, 4, mgr.viper.GetInt("engine.pool_prewarm_count"))
1919 assert.Equal(t, 256, mgr.viper.GetInt("engine.zoom_cache_size"))
2020 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_wheel_multiplier"), 0.001)
21- assert.InDelta(t, defaultCEFScrollTouchpadMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_touchpad_multiplier"), 0.001)
21+ assert.InDelta(t, defaultCEFScrollPreciseMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_precise_multiplier"), 0.001)
2222 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_horizontal_multiplier"), 0.001)
2323 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_vertical_multiplier"), 0.001)
2424 assert.Equal(t, defaultCEFScrollMaxDelta, mgr.viper.GetInt("engine.cef.input.scroll_max_delta"))
@@ -18,7 +18,7 @@ func TestSetEngineDefaults(t *testing.T) {
18 assert.Equal(t, 4, mgr.viper.GetInt("engine.pool_prewarm_count"))18 assert.Equal(t, 4, mgr.viper.GetInt("engine.pool_prewarm_count"))
19 assert.Equal(t, 256, mgr.viper.GetInt("engine.zoom_cache_size"))19 assert.Equal(t, 256, mgr.viper.GetInt("engine.zoom_cache_size"))
20 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_wheel_multiplier"), 0.001)20 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_wheel_multiplier"), 0.001)
21- assert.InDelta(t, defaultCEFScrollTouchpadMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_touchpad_multiplier"), 0.001)21+ assert.InDelta(t, defaultCEFScrollPreciseMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_precise_multiplier"), 0.001)
22 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_horizontal_multiplier"), 0.001)22 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_horizontal_multiplier"), 0.001)
23 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_vertical_multiplier"), 0.001)23 assert.InDelta(t, defaultCEFScrollMultiplier, mgr.viper.GetFloat64("engine.cef.input.scroll_vertical_multiplier"), 0.001)
24 assert.Equal(t, defaultCEFScrollMaxDelta, mgr.viper.GetInt("engine.cef.input.scroll_max_delta"))24 assert.Equal(t, defaultCEFScrollMaxDelta, mgr.viper.GetInt("engine.cef.input.scroll_max_delta"))
M internal/infrastructure/config/schema_provider.go +3 -3
@@ -971,10 +971,10 @@ func (*SchemaProvider) getPerformanceKeys(defaults *Config) []entity.ConfigKeyIn
971971 Section: SectionPerformance,
972972 },
973973 {
974- Key: "engine.cef.input.scroll_touchpad_multiplier",
974+ Key: "engine.cef.input.scroll_precise_multiplier",
975975 Type: "float64",
976- Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.ScrollTouchpadMultiplier),
977- Description: "CEF touchpad/smooth scroll delta multiplier",
976+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.ScrollPreciseMultiplier),
977+ Description: "CEF precise/surface scroll delta multiplier",
978978 Range: ">0",
979979 Section: SectionPerformance,
980980 },
@@ -971,10 +971,10 @@ func (*SchemaProvider) getPerformanceKeys(defaults *Config) []entity.ConfigKeyIn
971 Section: SectionPerformance,971 Section: SectionPerformance,
972 },972 },
973 {973 {
974- Key: "engine.cef.input.scroll_touchpad_multiplier",974+ Key: "engine.cef.input.scroll_precise_multiplier",
975 Type: "float64",975 Type: "float64",
976- Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.ScrollTouchpadMultiplier),976+ Default: fmt.Sprintf("%.2f", defaults.Engine.CEF.Input.ScrollPreciseMultiplier),
977- Description: "CEF touchpad/smooth scroll delta multiplier",977+ Description: "CEF precise/surface scroll delta multiplier",
978 Range: ">0",978 Range: ">0",
979 Section: SectionPerformance,979 Section: SectionPerformance,
980 },980 },
M internal/infrastructure/config/validation.go +2 -2
@@ -367,8 +367,8 @@ func validateCEF(config *Config) []string {
367367 if invalidPositiveFloat(input.ScrollWheelMultiplier) {
368368 validationErrors = append(validationErrors, "engine.cef.input.scroll_wheel_multiplier must be > 0")
369369 }
370- if invalidPositiveFloat(input.ScrollTouchpadMultiplier) {
371- validationErrors = append(validationErrors, "engine.cef.input.scroll_touchpad_multiplier must be > 0")
370+ if invalidPositiveFloat(input.ScrollPreciseMultiplier) {
371+ validationErrors = append(validationErrors, "engine.cef.input.scroll_precise_multiplier must be > 0")
372372 }
373373 if invalidPositiveFloat(input.ScrollHorizontalMultiplier) {
374374 validationErrors = append(validationErrors, "engine.cef.input.scroll_horizontal_multiplier must be > 0")
@@ -367,8 +367,8 @@ func validateCEF(config *Config) []string {
367 if invalidPositiveFloat(input.ScrollWheelMultiplier) {367 if invalidPositiveFloat(input.ScrollWheelMultiplier) {
368 validationErrors = append(validationErrors, "engine.cef.input.scroll_wheel_multiplier must be > 0")368 validationErrors = append(validationErrors, "engine.cef.input.scroll_wheel_multiplier must be > 0")
369 }369 }
370- if invalidPositiveFloat(input.ScrollTouchpadMultiplier) {370+ if invalidPositiveFloat(input.ScrollPreciseMultiplier) {
371- validationErrors = append(validationErrors, "engine.cef.input.scroll_touchpad_multiplier must be > 0")371+ validationErrors = append(validationErrors, "engine.cef.input.scroll_precise_multiplier must be > 0")
372 }372 }
373 if invalidPositiveFloat(input.ScrollHorizontalMultiplier) {373 if invalidPositiveFloat(input.ScrollHorizontalMultiplier) {
374 validationErrors = append(validationErrors, "engine.cef.input.scroll_horizontal_multiplier must be > 0")374 validationErrors = append(validationErrors, "engine.cef.input.scroll_horizontal_multiplier must be > 0")
M internal/infrastructure/config/validation_test.go +3 -3
@@ -81,12 +81,12 @@ func TestValidateConfig_CEFConfig(t *testing.T) {
8181 wantText: "engine.cef.input.scroll_wheel_multiplier",
8282 },
8383 {
84- name: "negative touchpad multiplier",
84+ name: "negative precise multiplier",
8585 mutate: func(cfg *Config) {
86- cfg.Engine.CEF.Input.ScrollTouchpadMultiplier = -0.1
86+ cfg.Engine.CEF.Input.ScrollPreciseMultiplier = -0.1
8787 },
8888 wantErr: true,
89- wantText: "engine.cef.input.scroll_touchpad_multiplier",
89+ wantText: "engine.cef.input.scroll_precise_multiplier",
9090 },
9191 {
9292 name: "zero horizontal multiplier",
@@ -81,12 +81,12 @@ func TestValidateConfig_CEFConfig(t *testing.T) {
81 wantText: "engine.cef.input.scroll_wheel_multiplier",81 wantText: "engine.cef.input.scroll_wheel_multiplier",
82 },82 },
83 {83 {
84- name: "negative touchpad multiplier",84+ name: "negative precise multiplier",
85 mutate: func(cfg *Config) {85 mutate: func(cfg *Config) {
86- cfg.Engine.CEF.Input.ScrollTouchpadMultiplier = -0.186+ cfg.Engine.CEF.Input.ScrollPreciseMultiplier = -0.1
87 },87 },
88 wantErr: true,88 wantErr: true,
89- wantText: "engine.cef.input.scroll_touchpad_multiplier",89+ wantText: "engine.cef.input.scroll_precise_multiplier",
90 },90 },
91 {91 {
92 name: "zero horizontal multiplier",92 name: "zero horizontal multiplier",
M internal/infrastructure/favicon/converter.go +2 -54
@@ -9,8 +9,6 @@ import (
99 "image/color"
1010 "image/png"
1111 "mime"
12- "regexp"
13- "strconv"
1412 "strings"
1513
1614 appport "github.com/bnema/dumber/internal/application/port"
@@ -53,7 +51,7 @@ func (*ImageConverter) Convert(ctx context.Context, original []byte, contentType
5351 func decodeFaviconImage(data []byte, ct string) (image.Image, error) {
5452 switch ct {
5553 case "image/svg+xml":
56- return renderSimpleSVG(data)
54+ return nil, fmt.Errorf("%w: svg favicon conversion is disabled", appport.ErrFaviconMiss)
5755 case "image/x-icon", "image/vnd.microsoft.icon", "image/ico":
5856 img, err := decodeICO(data)
5957 if err != nil {
@@ -61,7 +59,7 @@ func decodeFaviconImage(data []byte, ct string) (image.Image, error) {
6159 }
6260 return img, nil
6361 case "image/webp":
64- return renderWEBPPlaceholder(data)
62+ return nil, fmt.Errorf("%w: webp favicon conversion is disabled", appport.ErrFaviconMiss)
6563 }
6664 if ct != "" && ct != "image/png" && ct != "image/jpeg" && ct != "image/gif" && ct != "image/webp" {
6765 return nil, fmt.Errorf("%w: unsupported favicon content type %s", appport.ErrFaviconMiss, ct)
@@ -209,53 +207,3 @@ func decodeICODIB(data []byte, bitCountHint int) (image.Image, error) {
209207 }
210208 return img, nil
211209 }
212-
213-var svgSizeRe = regexp.MustCompile(`(?i)\b(width|height)=["']?(\d+)`) // intentionally small safe subset
214-var svgFillRe = regexp.MustCompile(`(?i)\bfill=["']#?([0-9a-f]{6})`)
215-
216-func renderWEBPPlaceholder(data []byte) (image.Image, error) {
217- if len(data) < 12 || !bytes.HasPrefix(data, []byte("RIFF")) || string(data[8:12]) != "WEBP" {
218- return nil, fmt.Errorf("%w: malformed webp", appport.ErrFaviconMiss)
219- }
220- return solidImage(64, color.RGBA{R: 128, G: 128, B: 128, A: 255}), nil
221-}
222-
223-func renderSimpleSVG(data []byte) (image.Image, error) {
224- trimmed := bytes.TrimSpace(data)
225- if !bytes.Contains(trimmed, []byte("<svg")) {
226- return nil, fmt.Errorf("%w: malformed svg", appport.ErrFaviconMiss)
227- }
228- size := 64
229- for _, m := range svgSizeRe.FindAllSubmatch(trimmed, -1) {
230- if v, err := strconv.Atoi(string(m[2])); err == nil && v > 0 && v <= 1024 {
231- size = v
232- break
233- }
234- }
235- fill := color.RGBA{R: 128, G: 128, B: 128, A: 255}
236- if m := svgFillRe.FindSubmatch(trimmed); len(m) == 2 {
237- if parsed, ok := parseHexColor(string(m[1])); ok {
238- fill = parsed
239- }
240- }
241- return solidImage(size, fill), nil
242-}
243-
244-func solidImage(size int, fill color.RGBA) image.Image {
245- img := image.NewRGBA(image.Rect(0, 0, size, size))
246- for y := 0; y < size; y++ {
247- for x := 0; x < size; x++ {
248- img.SetRGBA(x, y, fill)
249- }
250- }
251- return img
252-}
253-
254-func parseHexColor(hex string) (color.RGBA, bool) {
255- v, err := strconv.ParseUint(hex, 16, 32)
256- if err != nil {
257- return color.RGBA{}, false
258- }
259- const greenShift = 8
260- return color.RGBA{R: uint8(v >> 16), G: uint8(v >> greenShift), B: uint8(v), A: 255}, true
261-}
@@ -9,8 +9,6 @@ import (
9 "image/color"9 "image/color"
10 "image/png"10 "image/png"
11 "mime"11 "mime"
12- "regexp"
13- "strconv"
14 "strings"12 "strings"
15 13
16 appport "github.com/bnema/dumber/internal/application/port"14 appport "github.com/bnema/dumber/internal/application/port"
@@ -53,7 +51,7 @@ func (*ImageConverter) Convert(ctx context.Context, original []byte, contentType
53 func decodeFaviconImage(data []byte, ct string) (image.Image, error) {51 func decodeFaviconImage(data []byte, ct string) (image.Image, error) {
54 switch ct {52 switch ct {
55 case "image/svg+xml":53 case "image/svg+xml":
56- return renderSimpleSVG(data)54+ return nil, fmt.Errorf("%w: svg favicon conversion is disabled", appport.ErrFaviconMiss)
57 case "image/x-icon", "image/vnd.microsoft.icon", "image/ico":55 case "image/x-icon", "image/vnd.microsoft.icon", "image/ico":
58 img, err := decodeICO(data)56 img, err := decodeICO(data)
59 if err != nil {57 if err != nil {
@@ -61,7 +59,7 @@ func decodeFaviconImage(data []byte, ct string) (image.Image, error) {
61 }59 }
62 return img, nil60 return img, nil
63 case "image/webp":61 case "image/webp":
64- return renderWEBPPlaceholder(data)62+ return nil, fmt.Errorf("%w: webp favicon conversion is disabled", appport.ErrFaviconMiss)
65 }63 }
66 if ct != "" && ct != "image/png" && ct != "image/jpeg" && ct != "image/gif" && ct != "image/webp" {64 if ct != "" && ct != "image/png" && ct != "image/jpeg" && ct != "image/gif" && ct != "image/webp" {
67 return nil, fmt.Errorf("%w: unsupported favicon content type %s", appport.ErrFaviconMiss, ct)65 return nil, fmt.Errorf("%w: unsupported favicon content type %s", appport.ErrFaviconMiss, ct)
@@ -209,53 +207,3 @@ func decodeICODIB(data []byte, bitCountHint int) (image.Image, error) {
209 }207 }
210 return img, nil208 return img, nil
211 }209 }
212-
213-var svgSizeRe = regexp.MustCompile(`(?i)\b(width|height)=["']?(\d+)`) // intentionally small safe subset
214-var svgFillRe = regexp.MustCompile(`(?i)\bfill=["']#?([0-9a-f]{6})`)
215-
216-func renderWEBPPlaceholder(data []byte) (image.Image, error) {
217- if len(data) < 12 || !bytes.HasPrefix(data, []byte("RIFF")) || string(data[8:12]) != "WEBP" {
218- return nil, fmt.Errorf("%w: malformed webp", appport.ErrFaviconMiss)
219- }
220- return solidImage(64, color.RGBA{R: 128, G: 128, B: 128, A: 255}), nil
221-}
222-
223-func renderSimpleSVG(data []byte) (image.Image, error) {
224- trimmed := bytes.TrimSpace(data)
225- if !bytes.Contains(trimmed, []byte("<svg")) {
226- return nil, fmt.Errorf("%w: malformed svg", appport.ErrFaviconMiss)
227- }
228- size := 64
229- for _, m := range svgSizeRe.FindAllSubmatch(trimmed, -1) {
230- if v, err := strconv.Atoi(string(m[2])); err == nil && v > 0 && v <= 1024 {
231- size = v
232- break
233- }
234- }
235- fill := color.RGBA{R: 128, G: 128, B: 128, A: 255}
236- if m := svgFillRe.FindSubmatch(trimmed); len(m) == 2 {
237- if parsed, ok := parseHexColor(string(m[1])); ok {
238- fill = parsed
239- }
240- }
241- return solidImage(size, fill), nil
242-}
243-
244-func solidImage(size int, fill color.RGBA) image.Image {
245- img := image.NewRGBA(image.Rect(0, 0, size, size))
246- for y := 0; y < size; y++ {
247- for x := 0; x < size; x++ {
248- img.SetRGBA(x, y, fill)
249- }
250- }
251- return img
252-}
253-
254-func parseHexColor(hex string) (color.RGBA, bool) {
255- v, err := strconv.ParseUint(hex, 16, 32)
256- if err != nil {
257- return color.RGBA{}, false
258- }
259- const greenShift = 8
260- return color.RGBA{R: uint8(v >> 16), G: uint8(v >> greenShift), B: uint8(v), A: 255}, true
261-}
M internal/infrastructure/favicon/converter_test.go +17 -2
@@ -39,8 +39,6 @@ func TestConverterAcceptedFaviconFormatsProducePNGs(t *testing.T) {
3939 {"png", "image/png", testPNG(t, 16, 16)},
4040 {"ico with png payload", "image/x-icon", testICOWithPNG(t, testPNG(t, 16, 16))},
4141 {"ico with bitmap payload", "image/x-icon", testICOWithDIB(t)},
42- {"svg", "image/svg+xml", []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><rect width="16" height="16" fill="#ff0000"/></svg>`)},
43- {"webp placeholder", "image/webp", []byte("RIFF\x04\x00\x00\x00WEBPVP8 ")},
4442 } {
4543 t.Run(tc.name, func(t *testing.T) {
4644 out, err := conv.Convert(context.Background(), tc.data, tc.ct, []int{32})
@@ -61,6 +59,23 @@ func TestConverterAcceptedFaviconFormatsProducePNGs(t *testing.T) {
6159 }
6260 }
6361
62+func TestConverterRejectsFormatsThatWouldProducePlaceholders(t *testing.T) {
63+ conv := NewImageConverter()
64+ for _, tc := range []struct {
65+ name, ct string
66+ data []byte
67+ }{
68+ {"svg", "image/svg+xml", []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><rect width="16" height="16" fill="#ff0000"/></svg>`)},
69+ {"webp", "image/webp", []byte("RIFF\x04\x00\x00\x00WEBPVP8 ")},
70+ } {
71+ t.Run(tc.name, func(t *testing.T) {
72+ if _, err := conv.Convert(context.Background(), tc.data, tc.ct, []int{32}); !errors.Is(err, appport.ErrFaviconMiss) {
73+ t.Fatalf("err = %v, want favicon miss", err)
74+ }
75+ })
76+ }
77+}
78+
6479 func TestConverterMalformedInputsAreMisses(t *testing.T) {
6580 conv := NewImageConverter()
6681 for _, tc := range []struct {
@@ -39,8 +39,6 @@ func TestConverterAcceptedFaviconFormatsProducePNGs(t *testing.T) {
39 {"png", "image/png", testPNG(t, 16, 16)},39 {"png", "image/png", testPNG(t, 16, 16)},
40 {"ico with png payload", "image/x-icon", testICOWithPNG(t, testPNG(t, 16, 16))},40 {"ico with png payload", "image/x-icon", testICOWithPNG(t, testPNG(t, 16, 16))},
41 {"ico with bitmap payload", "image/x-icon", testICOWithDIB(t)},41 {"ico with bitmap payload", "image/x-icon", testICOWithDIB(t)},
42- {"svg", "image/svg+xml", []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><rect width="16" height="16" fill="#ff0000"/></svg>`)},
43- {"webp placeholder", "image/webp", []byte("RIFF\x04\x00\x00\x00WEBPVP8 ")},
44 } {42 } {
45 t.Run(tc.name, func(t *testing.T) {43 t.Run(tc.name, func(t *testing.T) {
46 out, err := conv.Convert(context.Background(), tc.data, tc.ct, []int{32})44 out, err := conv.Convert(context.Background(), tc.data, tc.ct, []int{32})
@@ -61,6 +59,23 @@ func TestConverterAcceptedFaviconFormatsProducePNGs(t *testing.T) {
61 }59 }
62 }60 }
63 61
62+func TestConverterRejectsFormatsThatWouldProducePlaceholders(t *testing.T) {
63+ conv := NewImageConverter()
64+ for _, tc := range []struct {
65+ name, ct string
66+ data []byte
67+ }{
68+ {"svg", "image/svg+xml", []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><rect width="16" height="16" fill="#ff0000"/></svg>`)},
69+ {"webp", "image/webp", []byte("RIFF\x04\x00\x00\x00WEBPVP8 ")},
70+ } {
71+ t.Run(tc.name, func(t *testing.T) {
72+ if _, err := conv.Convert(context.Background(), tc.data, tc.ct, []int{32}); !errors.Is(err, appport.ErrFaviconMiss) {
73+ t.Fatalf("err = %v, want favicon miss", err)
74+ }
75+ })
76+ }
77+}
78+
64 func TestConverterMalformedInputsAreMisses(t *testing.T) {79 func TestConverterMalformedInputsAreMisses(t *testing.T) {
65 conv := NewImageConverter()80 conv := NewImageConverter()
66 for _, tc := range []struct {81 for _, tc := range []struct {

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