feat(cef): auto-hiding scrollbar with theme color support
Inject custom scrollbar CSS and JS on all pages via the content injector: - Scrollbar invisible by default (transparent thumb) - Appears with --primary theme color on scroll/wheel activity - Widens from 6px to 10px on hover during scroll - Fades out after 1s of inactivity via .dumber-scrolling class toggle - Track transparent normally, 10% grey on hover - Rounded corners on thumb
- commit
- 991a91af8d668faf04760715659d7a42feab3c78
- author
- brice · · 2026-03-21 22:01 UTC
- committer
- brice · · 2026-03-28 04:38 UTC
- parent
- 179d874
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +54 -0
M internal/infrastructure/cef/content_injector.go +54 -0
| @@ -17,6 +17,56 @@ var _ port.ContentInjector = (*contentInjector)(nil) | |||
| 17 | 17 | // and message bridge scripts. Must match the engine's InternalSchemePath prefix. | |
| 18 | 18 | const internalSchemePrefix = "dumb://" | |
| 19 | 19 | ||
| 20 | + | // scrollbarCSS styles the scrollbar with auto-hide behavior: invisible by | |
| 21 | + | // default, fades in on scroll, widens on hover, fades out after 1s idle. | |
| 22 | + | // Uses --primary theme color for the thumb. | |
| 23 | + | const scrollbarCSS = ` | |
| 24 | + | ::-webkit-scrollbar { | |
| 25 | + | width: 6px; | |
| 26 | + | height: 6px; | |
| 27 | + | } | |
| 28 | + | ::-webkit-scrollbar-track { | |
| 29 | + | background: transparent; | |
| 30 | + | } | |
| 31 | + | ::-webkit-scrollbar-thumb { | |
| 32 | + | background: transparent; | |
| 33 | + | border-radius: 3px; | |
| 34 | + | transition: background 0.3s ease; | |
| 35 | + | } | |
| 36 | + | .dumber-scrolling ::-webkit-scrollbar-thumb { | |
| 37 | + | background: var(--primary, rgba(128, 128, 128, 0.4)); | |
| 38 | + | } | |
| 39 | + | .dumber-scrolling ::-webkit-scrollbar:hover { | |
| 40 | + | width: 10px; | |
| 41 | + | height: 10px; | |
| 42 | + | } | |
| 43 | + | .dumber-scrolling ::-webkit-scrollbar-track:hover { | |
| 44 | + | background: rgba(128, 128, 128, 0.1); | |
| 45 | + | } | |
| 46 | + | .dumber-scrolling ::-webkit-scrollbar-thumb:hover { | |
| 47 | + | background: var(--primary, rgba(128, 128, 128, 0.6)); | |
| 48 | + | } | |
| 49 | + | ::-webkit-scrollbar-corner { | |
| 50 | + | background: transparent; | |
| 51 | + | } | |
| 52 | + | ` | |
| 53 | + | ||
| 54 | + | // scrollbarAutoHideJS adds/removes the .dumber-scrolling class on <html> | |
| 55 | + | // on scroll activity, with a 1s fade-out timeout after scrolling stops. | |
| 56 | + | const scrollbarAutoHideJS = `(function(){ | |
| 57 | + | var t, el = document.documentElement; | |
| 58 | + | function show() { | |
| 59 | + | el.classList.add('dumber-scrolling'); | |
| 60 | + | clearTimeout(t); | |
| 61 | + | t = setTimeout(function(){ el.classList.remove('dumber-scrolling'); }, 1000); | |
| 62 | + | } | |
| 63 | + | window.addEventListener('scroll', show, {passive:true,capture:true}); | |
| 64 | + | window.addEventListener('wheel', show, {passive:true}); | |
| 65 | + | window.addEventListener('mouseenter', function(e){ | |
| 66 | + | if(el.scrollHeight > el.clientHeight) show(); | |
| 67 | + | }); | |
| 68 | + | })();` | |
| 69 | + | ||
| 20 | 70 | // contentInjector implements port.ContentInjector for the CEF engine. | |
| 21 | 71 | // It stores CSS strings and injects them into webviews via ExecuteJavaScript. | |
| 22 | 72 | // Thread-safe: InjectThemeCSS may be called from the UI thread while OnLoadEnd | |
| @@ -128,6 +178,10 @@ func (ci *contentInjector) onLoadEnd(wv *WebView) { | |||
| 128 | 178 | if findCSS != "" { | |
| 129 | 179 | ci.injectCSS(wv, "dumber-find-highlight", findCSS) | |
| 130 | 180 | } | |
| 181 | + | ||
| 182 | + | // All pages get custom scrollbar styling with auto-hide. | |
| 183 | + | ci.injectCSS(wv, "dumber-scrollbar", scrollbarCSS) | |
| 184 | + | wv.RunJavaScript(context.Background(), scrollbarAutoHideJS) | |
| 131 | 185 | } | |
| 132 | 186 | ||
| 133 | 187 | // injectCSS injects a CSS string as a <style> element via JavaScript. | |
| @@ -17,6 +17,56 @@ var _ port.ContentInjector = (*contentInjector)(nil) | |||||
| 17 | // and message bridge scripts. Must match the engine's InternalSchemePath prefix. | 17 | // and message bridge scripts. Must match the engine's InternalSchemePath prefix. | ||
| 18 | const internalSchemePrefix = "dumb://" | 18 | const internalSchemePrefix = "dumb://" | ||
| 19 | 19 | ||||
| 20 | + | // scrollbarCSS styles the scrollbar with auto-hide behavior: invisible by | |||
| 21 | + | // default, fades in on scroll, widens on hover, fades out after 1s idle. | |||
| 22 | + | // Uses --primary theme color for the thumb. | |||
| 23 | + | const scrollbarCSS = ` | |||
| 24 | + | ::-webkit-scrollbar { | |||
| 25 | + | width: 6px; | |||
| 26 | + | height: 6px; | |||
| 27 | + | } | |||
| 28 | + | ::-webkit-scrollbar-track { | |||
| 29 | + | background: transparent; | |||
| 30 | + | } | |||
| 31 | + | ::-webkit-scrollbar-thumb { | |||
| 32 | + | background: transparent; | |||
| 33 | + | border-radius: 3px; | |||
| 34 | + | transition: background 0.3s ease; | |||
| 35 | + | } | |||
| 36 | + | .dumber-scrolling ::-webkit-scrollbar-thumb { | |||
| 37 | + | background: var(--primary, rgba(128, 128, 128, 0.4)); | |||
| 38 | + | } | |||
| 39 | + | .dumber-scrolling ::-webkit-scrollbar:hover { | |||
| 40 | + | width: 10px; | |||
| 41 | + | height: 10px; | |||
| 42 | + | } | |||
| 43 | + | .dumber-scrolling ::-webkit-scrollbar-track:hover { | |||
| 44 | + | background: rgba(128, 128, 128, 0.1); | |||
| 45 | + | } | |||
| 46 | + | .dumber-scrolling ::-webkit-scrollbar-thumb:hover { | |||
| 47 | + | background: var(--primary, rgba(128, 128, 128, 0.6)); | |||
| 48 | + | } | |||
| 49 | + | ::-webkit-scrollbar-corner { | |||
| 50 | + | background: transparent; | |||
| 51 | + | } | |||
| 52 | + | ` | |||
| 53 | + | ||||
| 54 | + | // scrollbarAutoHideJS adds/removes the .dumber-scrolling class on <html> | |||
| 55 | + | // on scroll activity, with a 1s fade-out timeout after scrolling stops. | |||
| 56 | + | const scrollbarAutoHideJS = `(function(){ | |||
| 57 | + | var t, el = document.documentElement; | |||
| 58 | + | function show() { | |||
| 59 | + | el.classList.add('dumber-scrolling'); | |||
| 60 | + | clearTimeout(t); | |||
| 61 | + | t = setTimeout(function(){ el.classList.remove('dumber-scrolling'); }, 1000); | |||
| 62 | + | } | |||
| 63 | + | window.addEventListener('scroll', show, {passive:true,capture:true}); | |||
| 64 | + | window.addEventListener('wheel', show, {passive:true}); | |||
| 65 | + | window.addEventListener('mouseenter', function(e){ | |||
| 66 | + | if(el.scrollHeight > el.clientHeight) show(); | |||
| 67 | + | }); | |||
| 68 | + | })();` | |||
| 69 | + | ||||
| 20 | // contentInjector implements port.ContentInjector for the CEF engine. | 70 | // contentInjector implements port.ContentInjector for the CEF engine. | ||
| 21 | // It stores CSS strings and injects them into webviews via ExecuteJavaScript. | 71 | // It stores CSS strings and injects them into webviews via ExecuteJavaScript. | ||
| 22 | // Thread-safe: InjectThemeCSS may be called from the UI thread while OnLoadEnd | 72 | // Thread-safe: InjectThemeCSS may be called from the UI thread while OnLoadEnd | ||
| @@ -128,6 +178,10 @@ func (ci *contentInjector) onLoadEnd(wv *WebView) { | |||||
| 128 | if findCSS != "" { | 178 | if findCSS != "" { | ||
| 129 | ci.injectCSS(wv, "dumber-find-highlight", findCSS) | 179 | ci.injectCSS(wv, "dumber-find-highlight", findCSS) | ||
| 130 | } | 180 | } | ||
| 181 | + | ||||
| 182 | + | // All pages get custom scrollbar styling with auto-hide. | |||
| 183 | + | ci.injectCSS(wv, "dumber-scrollbar", scrollbarCSS) | |||
| 184 | + | wv.RunJavaScript(context.Background(), scrollbarAutoHideJS) | |||
| 131 | } | 185 | } | ||
| 132 | 186 | ||||
| 133 | // injectCSS injects a CSS string as a <style> element via JavaScript. | 187 | // injectCSS injects a CSS string as a <style> element via JavaScript. | ||