fix(ui): address CodeRabbit feedback across floating-pane paths
Harden floating-pane resize watching and GTK-thread favicon preload behavior while improving shortcut alias handling and logging safety. Refine related docs, tests, and config validation paths for clearer behavior and maintainability.
- commit
- 4959fb82cb5f808807270a02cda15b38d9040847
- author
- brice · · 2026-02-09 12:19 UTC
- parent
- af377b4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
15 files · +224 -116
M docs/getting-started.md +1 -1
| @@ -38,7 +38,7 @@ Press `Ctrl+L` to open the omnibox for: | |||
| 38 | 38 | ## Floating Pane | |
| 39 | 39 | ||
| 40 | 40 | - Press `Alt+F` to toggle the floating pane. | |
| 41 | - | - Press `Ctrl+W` to close the active pane; if floating is active, this fully releases it for a fresh next open. | |
| 41 | + | - Press `Ctrl+W` to close the active pane; if the floating pane is active, this fully releases it so the next open starts fresh. | |
| 42 | 42 | - Some `Alt+<key>` bindings can conflict with default WebKit shortcuts or desktop-level handlers. | |
| 43 | 43 | ||
| 44 | 44 | See [Floating Pane](./reference/floating-pane.md) for profile shortcuts and configuration. | |
| @@ -38,7 +38,7 @@ Press `Ctrl+L` to open the omnibox for: | |||||
| 38 | ## Floating Pane | 38 | ## Floating Pane | ||
| 39 | 39 | ||||
| 40 | - Press `Alt+F` to toggle the floating pane. | 40 | - Press `Alt+F` to toggle the floating pane. | ||
| 41 | - | - Press `Ctrl+W` to close the active pane; if floating is active, this fully releases it for a fresh next open. | 41 | + | - Press `Ctrl+W` to close the active pane; if the floating pane is active, this fully releases it so the next open starts fresh. |
| 42 | - Some `Alt+<key>` bindings can conflict with default WebKit shortcuts or desktop-level handlers. | 42 | - Some `Alt+<key>` bindings can conflict with default WebKit shortcuts or desktop-level handlers. | ||
| 43 | 43 | ||||
| 44 | See [Floating Pane](./reference/floating-pane.md) for profile shortcuts and configuration. | 44 | See [Floating Pane](./reference/floating-pane.md) for profile shortcuts and configuration. | ||
M internal/infrastructure/config/floating_pane_config_test.go +73 -63
| @@ -85,70 +85,80 @@ func TestFloatingPaneConfig_ValidationRanges(t *testing.T) { | |||
| 85 | 85 | } | |
| 86 | 86 | ||
| 87 | 87 | func TestFloatingPaneConfig_ProfileValidation(t *testing.T) { | |
| 88 | - | cfg := DefaultConfig() | |
| 89 | - | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | |
| 90 | - | "google": { | |
| 91 | - | URL: "", | |
| 92 | - | Keys: []string{"alt+g"}, | |
| 93 | - | }, | |
| 94 | - | } | |
| 95 | - | ||
| 96 | - | err := validateConfig(cfg) | |
| 97 | - | require.Error(t, err) | |
| 98 | - | assert.Contains(t, err.Error(), "workspace.floating_pane.profiles.google.url") | |
| 99 | - | ||
| 100 | - | cfg = DefaultConfig() | |
| 101 | - | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | |
| 102 | - | "google": { | |
| 103 | - | URL: "https://google.com", | |
| 104 | - | Keys: nil, | |
| 105 | - | }, | |
| 106 | - | } | |
| 107 | - | ||
| 108 | - | err = validateConfig(cfg) | |
| 109 | - | require.Error(t, err) | |
| 110 | - | assert.Contains(t, err.Error(), "workspace.floating_pane.profiles.google.keys") | |
| 111 | - | ||
| 112 | - | cfg = DefaultConfig() | |
| 113 | - | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | |
| 114 | - | "google": { | |
| 115 | - | URL: "https://google.com", | |
| 116 | - | Keys: []string{" ", "alt+g"}, | |
| 117 | - | }, | |
| 118 | - | } | |
| 119 | - | ||
| 120 | - | err = validateConfig(cfg) | |
| 121 | - | require.Error(t, err) | |
| 122 | - | assert.Contains(t, err.Error(), "empty or whitespace-only key binding") | |
| 123 | - | ||
| 124 | - | cfg = DefaultConfig() | |
| 125 | - | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | |
| 126 | - | "google": { | |
| 127 | - | URL: "https://google.com", | |
| 128 | - | Keys: []string{"alt+g"}, | |
| 129 | - | }, | |
| 130 | - | "github": { | |
| 131 | - | URL: "https://github.com", | |
| 132 | - | Keys: []string{"alt+g"}, | |
| 133 | - | }, | |
| 134 | - | } | |
| 135 | - | ||
| 136 | - | err = validateConfig(cfg) | |
| 137 | - | require.Error(t, err) | |
| 138 | - | assert.Contains(t, err.Error(), "duplicate key binding") | |
| 139 | - | ||
| 140 | - | cfg = DefaultConfig() | |
| 141 | - | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | |
| 142 | - | "invalid-scheme": { | |
| 143 | - | URL: "ftp://example.com", | |
| 144 | - | Keys: []string{"alt+i"}, | |
| 145 | - | }, | |
| 146 | - | } | |
| 88 | + | t.Run("missing URL", func(t *testing.T) { | |
| 89 | + | cfg := DefaultConfig() | |
| 90 | + | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | |
| 91 | + | "google": { | |
| 92 | + | URL: "", | |
| 93 | + | Keys: []string{"alt+g"}, | |
| 94 | + | }, | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | err := validateConfig(cfg) | |
| 98 | + | require.Error(t, err) | |
| 99 | + | assert.Contains(t, err.Error(), "workspace.floating_pane.profiles.google.url") | |
| 100 | + | }) | |
| 101 | + | ||
| 102 | + | t.Run("missing keys", func(t *testing.T) { | |
| 103 | + | cfg := DefaultConfig() | |
| 104 | + | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | |
| 105 | + | "google": { | |
| 106 | + | URL: "https://google.com", | |
| 107 | + | Keys: nil, | |
| 108 | + | }, | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | err := validateConfig(cfg) | |
| 112 | + | require.Error(t, err) | |
| 113 | + | assert.Contains(t, err.Error(), "workspace.floating_pane.profiles.google.keys") | |
| 114 | + | }) | |
| 115 | + | ||
| 116 | + | t.Run("whitespace-only key", func(t *testing.T) { | |
| 117 | + | cfg := DefaultConfig() | |
| 118 | + | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | |
| 119 | + | "google": { | |
| 120 | + | URL: "https://google.com", | |
| 121 | + | Keys: []string{" ", "alt+g"}, | |
| 122 | + | }, | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | err := validateConfig(cfg) | |
| 126 | + | require.Error(t, err) | |
| 127 | + | assert.Contains(t, err.Error(), "empty or whitespace-only key binding") | |
| 128 | + | }) | |
| 129 | + | ||
| 130 | + | t.Run("duplicate key binding", func(t *testing.T) { | |
| 131 | + | cfg := DefaultConfig() | |
| 132 | + | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | |
| 133 | + | "google": { | |
| 134 | + | URL: "https://google.com", | |
| 135 | + | Keys: []string{"alt+g"}, | |
| 136 | + | }, | |
| 137 | + | "github": { | |
| 138 | + | URL: "https://github.com", | |
| 139 | + | Keys: []string{"alt+g"}, | |
| 140 | + | }, | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | err := validateConfig(cfg) | |
| 144 | + | require.Error(t, err) | |
| 145 | + | assert.Contains(t, err.Error(), "duplicate key binding") | |
| 146 | + | }) | |
| 147 | + | ||
| 148 | + | t.Run("invalid URL scheme", func(t *testing.T) { | |
| 149 | + | cfg := DefaultConfig() | |
| 150 | + | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | |
| 151 | + | "invalid-scheme": { | |
| 152 | + | URL: "ftp://example.com", | |
| 153 | + | Keys: []string{"alt+i"}, | |
| 154 | + | }, | |
| 155 | + | } | |
| 147 | 156 | ||
| 148 | - | err = validateConfig(cfg) | |
| 149 | - | require.Error(t, err) | |
| 150 | - | assert.Contains(t, err.Error(), "workspace.floating_pane.profiles.invalid-scheme.url") | |
| 151 | - | assert.Contains(t, err.Error(), "must use one of: http, https, dumb, file, about") | |
| 157 | + | err := validateConfig(cfg) | |
| 158 | + | require.Error(t, err) | |
| 159 | + | assert.Contains(t, err.Error(), "workspace.floating_pane.profiles.invalid-scheme.url") | |
| 160 | + | assert.Contains(t, err.Error(), "must use one of: http, https, dumb, file, about") | |
| 161 | + | }) | |
| 152 | 162 | } | |
| 153 | 163 | ||
| 154 | 164 | func TestFloatingPaneConfig_ProfileMultiplicity(t *testing.T) { | |
| @@ -85,70 +85,80 @@ func TestFloatingPaneConfig_ValidationRanges(t *testing.T) { | |||||
| 85 | } | 85 | } | ||
| 86 | 86 | ||||
| 87 | func TestFloatingPaneConfig_ProfileValidation(t *testing.T) { | 87 | func TestFloatingPaneConfig_ProfileValidation(t *testing.T) { | ||
| 88 | - | cfg := DefaultConfig() | 88 | + | t.Run("missing URL", func(t *testing.T) { |
| 89 | - | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | 89 | + | cfg := DefaultConfig() |
| 90 | - | "google": { | 90 | + | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ |
| 91 | - | URL: "", | 91 | + | "google": { |
| 92 | - | Keys: []string{"alt+g"}, | 92 | + | URL: "", |
| 93 | - | }, | 93 | + | Keys: []string{"alt+g"}, |
| 94 | - | } | 94 | + | }, |
| 95 | - | 95 | + | } | |
| 96 | - | err := validateConfig(cfg) | 96 | + | |
| 97 | - | require.Error(t, err) | 97 | + | err := validateConfig(cfg) |
| 98 | - | assert.Contains(t, err.Error(), "workspace.floating_pane.profiles.google.url") | 98 | + | require.Error(t, err) |
| 99 | - | 99 | + | assert.Contains(t, err.Error(), "workspace.floating_pane.profiles.google.url") | |
| 100 | - | cfg = DefaultConfig() | 100 | + | }) |
| 101 | - | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | 101 | + | |
| 102 | - | "google": { | 102 | + | t.Run("missing keys", func(t *testing.T) { |
| 103 | - | URL: "https://google.com", | 103 | + | cfg := DefaultConfig() |
| 104 | - | Keys: nil, | 104 | + | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ |
| 105 | - | }, | 105 | + | "google": { |
| 106 | - | } | 106 | + | URL: "https://google.com", |
| 107 | - | 107 | + | Keys: nil, | |
| 108 | - | err = validateConfig(cfg) | 108 | + | }, |
| 109 | - | require.Error(t, err) | 109 | + | } |
| 110 | - | assert.Contains(t, err.Error(), "workspace.floating_pane.profiles.google.keys") | 110 | + | |
| 111 | - | 111 | + | err := validateConfig(cfg) | |
| 112 | - | cfg = DefaultConfig() | 112 | + | require.Error(t, err) |
| 113 | - | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | 113 | + | assert.Contains(t, err.Error(), "workspace.floating_pane.profiles.google.keys") |
| 114 | - | "google": { | 114 | + | }) |
| 115 | - | URL: "https://google.com", | 115 | + | |
| 116 | - | Keys: []string{" ", "alt+g"}, | 116 | + | t.Run("whitespace-only key", func(t *testing.T) { |
| 117 | - | }, | 117 | + | cfg := DefaultConfig() |
| 118 | - | } | 118 | + | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ |
| 119 | - | 119 | + | "google": { | |
| 120 | - | err = validateConfig(cfg) | 120 | + | URL: "https://google.com", |
| 121 | - | require.Error(t, err) | 121 | + | Keys: []string{" ", "alt+g"}, |
| 122 | - | assert.Contains(t, err.Error(), "empty or whitespace-only key binding") | 122 | + | }, |
| 123 | - | 123 | + | } | |
| 124 | - | cfg = DefaultConfig() | 124 | + | |
| 125 | - | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | 125 | + | err := validateConfig(cfg) |
| 126 | - | "google": { | 126 | + | require.Error(t, err) |
| 127 | - | URL: "https://google.com", | 127 | + | assert.Contains(t, err.Error(), "empty or whitespace-only key binding") |
| 128 | - | Keys: []string{"alt+g"}, | 128 | + | }) |
| 129 | - | }, | 129 | + | |
| 130 | - | "github": { | 130 | + | t.Run("duplicate key binding", func(t *testing.T) { |
| 131 | - | URL: "https://github.com", | 131 | + | cfg := DefaultConfig() |
| 132 | - | Keys: []string{"alt+g"}, | 132 | + | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ |
| 133 | - | }, | 133 | + | "google": { |
| 134 | - | } | 134 | + | URL: "https://google.com", |
| 135 | - | 135 | + | Keys: []string{"alt+g"}, | |
| 136 | - | err = validateConfig(cfg) | 136 | + | }, |
| 137 | - | require.Error(t, err) | 137 | + | "github": { |
| 138 | - | assert.Contains(t, err.Error(), "duplicate key binding") | 138 | + | URL: "https://github.com", |
| 139 | - | 139 | + | Keys: []string{"alt+g"}, | |
| 140 | - | cfg = DefaultConfig() | 140 | + | }, |
| 141 | - | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | 141 | + | } |
| 142 | - | "invalid-scheme": { | 142 | + | |
| 143 | - | URL: "ftp://example.com", | 143 | + | err := validateConfig(cfg) |
| 144 | - | Keys: []string{"alt+i"}, | 144 | + | require.Error(t, err) |
| 145 | - | }, | 145 | + | assert.Contains(t, err.Error(), "duplicate key binding") |
| 146 | - | } | 146 | + | }) |
| 147 | + | ||||
| 148 | + | t.Run("invalid URL scheme", func(t *testing.T) { | |||
| 149 | + | cfg := DefaultConfig() | |||
| 150 | + | cfg.Workspace.FloatingPane.Profiles = map[string]FloatingPaneProfile{ | |||
| 151 | + | "invalid-scheme": { | |||
| 152 | + | URL: "ftp://example.com", | |||
| 153 | + | Keys: []string{"alt+i"}, | |||
| 154 | + | }, | |||
| 155 | + | } | |||
| 147 | 156 | ||||
| 148 | - | err = validateConfig(cfg) | 157 | + | err := validateConfig(cfg) |
| 149 | - | require.Error(t, err) | 158 | + | require.Error(t, err) |
| 150 | - | assert.Contains(t, err.Error(), "workspace.floating_pane.profiles.invalid-scheme.url") | 159 | + | assert.Contains(t, err.Error(), "workspace.floating_pane.profiles.invalid-scheme.url") |
| 151 | - | assert.Contains(t, err.Error(), "must use one of: http, https, dumb, file, about") | 160 | + | assert.Contains(t, err.Error(), "must use one of: http, https, dumb, file, about") |
| 161 | + | }) | |||
| 152 | } | 162 | } | ||
| 153 | 163 | ||||
| 154 | func TestFloatingPaneConfig_ProfileMultiplicity(t *testing.T) { | 164 | func TestFloatingPaneConfig_ProfileMultiplicity(t *testing.T) { | ||
M internal/infrastructure/config/migrate.go +2 -0
| @@ -835,6 +835,8 @@ func (*Migrator) toStringAnyMap(value any) (map[string]any, bool) { | |||
| 835 | 835 | return m, true | |
| 836 | 836 | } | |
| 837 | 837 | ||
| 838 | + | // Some TOML/YAML parser paths yield non-map[string]any map implementations | |
| 839 | + | // for nested tables, so keep a reflect.Map coercion fallback here. | |
| 838 | 840 | rv := reflect.ValueOf(value) | |
| 839 | 841 | if rv.Kind() != reflect.Map { | |
| 840 | 842 | return nil, false | |
| @@ -835,6 +835,8 @@ func (*Migrator) toStringAnyMap(value any) (map[string]any, bool) { | |||||
| 835 | return m, true | 835 | return m, true | ||
| 836 | } | 836 | } | ||
| 837 | 837 | ||||
| 838 | + | // Some TOML/YAML parser paths yield non-map[string]any map implementations | |||
| 839 | + | // for nested tables, so keep a reflect.Map coercion fallback here. | |||
| 838 | rv := reflect.ValueOf(value) | 840 | rv := reflect.ValueOf(value) | ||
| 839 | if rv.Kind() != reflect.Map { | 841 | if rv.Kind() != reflect.Map { | ||
| 840 | return nil, false | 842 | return nil, false | ||
M internal/infrastructure/config/validation.go +5 -6
| @@ -334,13 +334,12 @@ func validateWorkspaceNewPaneURL(config *Config) []string { | |||
| 334 | 334 | ||
| 335 | 335 | func validateWorkspaceURLValue(fieldPath, value string) []string { | |
| 336 | 336 | trimmed := strings.TrimSpace(value) | |
| 337 | - | normalized := domainurl.Normalize(trimmed) | |
| 338 | - | candidate := normalized | |
| 339 | - | if rawParsed, rawErr := url.Parse(trimmed); rawErr == nil && rawParsed.Scheme != "" { | |
| 340 | - | candidate = trimmed | |
| 341 | - | } | |
| 342 | - | ||
| 337 | + | candidate := trimmed | |
| 343 | 338 | parsed, err := url.Parse(candidate) | |
| 339 | + | if err != nil || parsed.Scheme == "" { | |
| 340 | + | candidate = domainurl.Normalize(trimmed) | |
| 341 | + | parsed, err = url.Parse(candidate) | |
| 342 | + | } | |
| 344 | 343 | if err != nil { | |
| 345 | 344 | return []string{fmt.Sprintf("%s must be a valid URL (got: %s)", fieldPath, value)} | |
| 346 | 345 | } | |
| @@ -334,13 +334,12 @@ func validateWorkspaceNewPaneURL(config *Config) []string { | |||||
| 334 | 334 | ||||
| 335 | func validateWorkspaceURLValue(fieldPath, value string) []string { | 335 | func validateWorkspaceURLValue(fieldPath, value string) []string { | ||
| 336 | trimmed := strings.TrimSpace(value) | 336 | trimmed := strings.TrimSpace(value) | ||
| 337 | - | normalized := domainurl.Normalize(trimmed) | 337 | + | candidate := trimmed |
| 338 | - | candidate := normalized | |||
| 339 | - | if rawParsed, rawErr := url.Parse(trimmed); rawErr == nil && rawParsed.Scheme != "" { | |||
| 340 | - | candidate = trimmed | |||
| 341 | - | } | |||
| 342 | - | ||||
| 343 | parsed, err := url.Parse(candidate) | 338 | parsed, err := url.Parse(candidate) | ||
| 339 | + | if err != nil || parsed.Scheme == "" { | |||
| 340 | + | candidate = domainurl.Normalize(trimmed) | |||
| 341 | + | parsed, err = url.Parse(candidate) | |||
| 342 | + | } | |||
| 344 | if err != nil { | 343 | if err != nil { | ||
| 345 | return []string{fmt.Sprintf("%s must be a valid URL (got: %s)", fieldPath, value)} | 344 | return []string{fmt.Sprintf("%s must be a valid URL (got: %s)", fieldPath, value)} | ||
| 346 | } | 345 | } | ||
M internal/ui/app.go +38 -3
| @@ -2183,7 +2183,21 @@ func floatingAllocationRect(overlayWidth, overlayHeight, desiredWidth, desiredHe | |||
| 2183 | 2183 | return x, y, width, height, true | |
| 2184 | 2184 | } | |
| 2185 | 2185 | ||
| 2186 | - | //nolint:gosec // GtkOverlay get-child-position provides a valid GtkAllocation pointer owned by GTK. | |
| 2186 | + | type gtkAllocationLayout [4]int32 | |
| 2187 | + | ||
| 2188 | + | const gtkAllocationLayoutSize = 16 | |
| 2189 | + | ||
| 2190 | + | var ( | |
| 2191 | + | _ [gtkAllocationLayoutSize - int(unsafe.Sizeof(gtkAllocationLayout{}))]struct{} | |
| 2192 | + | _ [int(unsafe.Sizeof(gtkAllocationLayout{})) - gtkAllocationLayoutSize]struct{} | |
| 2193 | + | ) | |
| 2194 | + | ||
| 2195 | + | // writeOverlayAllocation writes GTK-owned GtkAllocation fields in place. | |
| 2196 | + | // Assumes GTK4 C layout for GdkRectangle/GtkAllocation: four contiguous | |
| 2197 | + | // 32-bit signed integers {x, y, width, height} in native endianness. | |
| 2198 | + | // If GTK struct layout changes, revisit offsets and writes below. | |
| 2199 | + | // | |
| 2200 | + | //nolint:gosec // Intentional unsafe pointer math against GTK-owned allocation memory. | |
| 2187 | 2201 | func writeOverlayAllocation(allocationPtr *uintptr, x, y, width, height int) bool { | |
| 2188 | 2202 | if allocationPtr == nil { | |
| 2189 | 2203 | return false | |
| @@ -2604,6 +2618,19 @@ func (a *App) handleFloatingViewportTick(session *floatingWorkspaceSession) bool | |||
| 2604 | 2618 | return true | |
| 2605 | 2619 | } | |
| 2606 | 2620 | ||
| 2621 | + | func (a *App) floatingSessionByPaneID(paneID entity.PaneID) *floatingWorkspaceSession { | |
| 2622 | + | if paneID == "" { | |
| 2623 | + | return nil | |
| 2624 | + | } | |
| 2625 | + | for _, session := range a.floatingSessions { | |
| 2626 | + | if session == nil || session.paneID != paneID { | |
| 2627 | + | continue | |
| 2628 | + | } | |
| 2629 | + | return session | |
| 2630 | + | } | |
| 2631 | + | return nil | |
| 2632 | + | } | |
| 2633 | + | ||
| 2607 | 2634 | func (a *App) startFloatingResizeWatcher(session *floatingWorkspaceSession) { | |
| 2608 | 2635 | if session == nil || session.overlay == nil || session.resizeWatcherActive { | |
| 2609 | 2636 | return | |
| @@ -2615,11 +2642,19 @@ func (a *App) startFloatingResizeWatcher(session *floatingWorkspaceSession) { | |||
| 2615 | 2642 | } | |
| 2616 | 2643 | ||
| 2617 | 2644 | session.resizeWatcherActive = true | |
| 2645 | + | paneID := session.paneID | |
| 2618 | 2646 | tickCallback := gtk.TickCallback(func(_ uintptr, _ uintptr, _ uintptr) bool { | |
| 2619 | - | keepRunning := a.handleFloatingViewportTick(session) | |
| 2620 | - | if !keepRunning { | |
| 2647 | + | liveSession := a.floatingSessionByPaneID(paneID) | |
| 2648 | + | if liveSession == nil { | |
| 2621 | 2649 | session.resizeWatcherActive = false | |
| 2622 | 2650 | session.resizeTickID = 0 | |
| 2651 | + | return false | |
| 2652 | + | } | |
| 2653 | + | ||
| 2654 | + | keepRunning := a.handleFloatingViewportTick(liveSession) | |
| 2655 | + | if !keepRunning { | |
| 2656 | + | liveSession.resizeWatcherActive = false | |
| 2657 | + | liveSession.resizeTickID = 0 | |
| 2623 | 2658 | } | |
| 2624 | 2659 | return keepRunning | |
| 2625 | 2660 | }) | |
| @@ -2183,7 +2183,21 @@ func floatingAllocationRect(overlayWidth, overlayHeight, desiredWidth, desiredHe | |||||
| 2183 | return x, y, width, height, true | 2183 | return x, y, width, height, true | ||
| 2184 | } | 2184 | } | ||
| 2185 | 2185 | ||||
| 2186 | - | //nolint:gosec // GtkOverlay get-child-position provides a valid GtkAllocation pointer owned by GTK. | 2186 | + | type gtkAllocationLayout [4]int32 |
| 2187 | + | ||||
| 2188 | + | const gtkAllocationLayoutSize = 16 | |||
| 2189 | + | ||||
| 2190 | + | var ( | |||
| 2191 | + | _ [gtkAllocationLayoutSize - int(unsafe.Sizeof(gtkAllocationLayout{}))]struct{} | |||
| 2192 | + | _ [int(unsafe.Sizeof(gtkAllocationLayout{})) - gtkAllocationLayoutSize]struct{} | |||
| 2193 | + | ) | |||
| 2194 | + | ||||
| 2195 | + | // writeOverlayAllocation writes GTK-owned GtkAllocation fields in place. | |||
| 2196 | + | // Assumes GTK4 C layout for GdkRectangle/GtkAllocation: four contiguous | |||
| 2197 | + | // 32-bit signed integers {x, y, width, height} in native endianness. | |||
| 2198 | + | // If GTK struct layout changes, revisit offsets and writes below. | |||
| 2199 | + | // | |||
| 2200 | + | //nolint:gosec // Intentional unsafe pointer math against GTK-owned allocation memory. | |||
| 2187 | func writeOverlayAllocation(allocationPtr *uintptr, x, y, width, height int) bool { | 2201 | func writeOverlayAllocation(allocationPtr *uintptr, x, y, width, height int) bool { | ||
| 2188 | if allocationPtr == nil { | 2202 | if allocationPtr == nil { | ||
| 2189 | return false | 2203 | return false | ||
| @@ -2604,6 +2618,19 @@ func (a *App) handleFloatingViewportTick(session *floatingWorkspaceSession) bool | |||||
| 2604 | return true | 2618 | return true | ||
| 2605 | } | 2619 | } | ||
| 2606 | 2620 | ||||
| 2621 | + | func (a *App) floatingSessionByPaneID(paneID entity.PaneID) *floatingWorkspaceSession { | |||
| 2622 | + | if paneID == "" { | |||
| 2623 | + | return nil | |||
| 2624 | + | } | |||
| 2625 | + | for _, session := range a.floatingSessions { | |||
| 2626 | + | if session == nil || session.paneID != paneID { | |||
| 2627 | + | continue | |||
| 2628 | + | } | |||
| 2629 | + | return session | |||
| 2630 | + | } | |||
| 2631 | + | return nil | |||
| 2632 | + | } | |||
| 2633 | + | ||||
| 2607 | func (a *App) startFloatingResizeWatcher(session *floatingWorkspaceSession) { | 2634 | func (a *App) startFloatingResizeWatcher(session *floatingWorkspaceSession) { | ||
| 2608 | if session == nil || session.overlay == nil || session.resizeWatcherActive { | 2635 | if session == nil || session.overlay == nil || session.resizeWatcherActive { | ||
| 2609 | return | 2636 | return | ||
| @@ -2615,11 +2642,19 @@ func (a *App) startFloatingResizeWatcher(session *floatingWorkspaceSession) { | |||||
| 2615 | } | 2642 | } | ||
| 2616 | 2643 | ||||
| 2617 | session.resizeWatcherActive = true | 2644 | session.resizeWatcherActive = true | ||
| 2645 | + | paneID := session.paneID | |||
| 2618 | tickCallback := gtk.TickCallback(func(_ uintptr, _ uintptr, _ uintptr) bool { | 2646 | tickCallback := gtk.TickCallback(func(_ uintptr, _ uintptr, _ uintptr) bool { | ||
| 2619 | - | keepRunning := a.handleFloatingViewportTick(session) | 2647 | + | liveSession := a.floatingSessionByPaneID(paneID) |
| 2620 | - | if !keepRunning { | 2648 | + | if liveSession == nil { |
| 2621 | session.resizeWatcherActive = false | 2649 | session.resizeWatcherActive = false | ||
| 2622 | session.resizeTickID = 0 | 2650 | session.resizeTickID = 0 | ||
| 2651 | + | return false | |||
| 2652 | + | } | |||
| 2653 | + | ||||
| 2654 | + | keepRunning := a.handleFloatingViewportTick(liveSession) | |||
| 2655 | + | if !keepRunning { | |||
| 2656 | + | liveSession.resizeWatcherActive = false | |||
| 2657 | + | liveSession.resizeTickID = 0 | |||
| 2623 | } | 2658 | } | ||
| 2624 | return keepRunning | 2659 | return keepRunning | ||
| 2625 | }) | 2660 | }) | ||
M internal/ui/component/floating_pane.go +11 -20
| @@ -78,6 +78,8 @@ func (fp *FloatingPane) ShowToggle(ctx context.Context) error { | |||
| 78 | 78 | showOmnibox := navigateToBlank || fp.currentURL == defaultFloatingPaneURL | |
| 79 | 79 | fp.visible = true | |
| 80 | 80 | fp.omniboxVisible = showOmnibox | |
| 81 | + | // Release lock before Navigate: callbacks may block and must not run while | |
| 82 | + | // holding fp.mu. On error, rollback below restores visibility state. | |
| 81 | 83 | fp.mu.Unlock() | |
| 82 | 84 | ||
| 83 | 85 | if navigateToBlank { | |
| @@ -130,7 +132,7 @@ func (fp *FloatingPane) ShowURL(ctx context.Context, url string) error { | |||
| 130 | 132 | } | |
| 131 | 133 | ||
| 132 | 134 | // Hide hides the pane while preserving web session state. | |
| 133 | - | func (fp *FloatingPane) Hide(context.Context) { | |
| 135 | + | func (fp *FloatingPane) Hide(_ context.Context) { | |
| 134 | 136 | fp.mu.Lock() | |
| 135 | 137 | defer fp.mu.Unlock() | |
| 136 | 138 | fp.visible = false | |
| @@ -164,26 +166,15 @@ func (fp *FloatingPane) Navigate(ctx context.Context, url string) error { | |||
| 164 | 166 | ||
| 165 | 167 | // Resize recalculates pane dimensions from parent overlay allocation. | |
| 166 | 168 | func (fp *FloatingPane) Resize() { | |
| 167 | - | fp.mu.RLock() | |
| 168 | - | parent := fp.parent | |
| 169 | - | widthPct := fp.widthPct | |
| 170 | - | heightPct := fp.heightPct | |
| 171 | - | fallbackWidth := fp.fallbackWidth | |
| 172 | - | fallbackHeight := fp.fallbackHeight | |
| 173 | - | fp.mu.RUnlock() | |
| 174 | - | ||
| 175 | - | width, height := CalculateOverlayDimensions( | |
| 176 | - | parent, | |
| 177 | - | widthPct, | |
| 178 | - | heightPct, | |
| 179 | - | fallbackWidth, | |
| 180 | - | fallbackHeight, | |
| 181 | - | ) | |
| 182 | - | ||
| 183 | 169 | fp.mu.Lock() | |
| 184 | - | fp.calculatedWidth = width | |
| 185 | - | fp.calculatedHeight = height | |
| 186 | - | fp.mu.Unlock() | |
| 170 | + | defer fp.mu.Unlock() | |
| 171 | + | fp.calculatedWidth, fp.calculatedHeight = CalculateOverlayDimensions( | |
| 172 | + | fp.parent, | |
| 173 | + | fp.widthPct, | |
| 174 | + | fp.heightPct, | |
| 175 | + | fp.fallbackWidth, | |
| 176 | + | fp.fallbackHeight, | |
| 177 | + | ) | |
| 187 | 178 | } | |
| 188 | 179 | ||
| 189 | 180 | // SetParentOverlay updates the workspace overlay used for sizing calculations. | |
| @@ -78,6 +78,8 @@ func (fp *FloatingPane) ShowToggle(ctx context.Context) error { | |||||
| 78 | showOmnibox := navigateToBlank || fp.currentURL == defaultFloatingPaneURL | 78 | showOmnibox := navigateToBlank || fp.currentURL == defaultFloatingPaneURL | ||
| 79 | fp.visible = true | 79 | fp.visible = true | ||
| 80 | fp.omniboxVisible = showOmnibox | 80 | fp.omniboxVisible = showOmnibox | ||
| 81 | + | // Release lock before Navigate: callbacks may block and must not run while | |||
| 82 | + | // holding fp.mu. On error, rollback below restores visibility state. | |||
| 81 | fp.mu.Unlock() | 83 | fp.mu.Unlock() | ||
| 82 | 84 | ||||
| 83 | if navigateToBlank { | 85 | if navigateToBlank { | ||
| @@ -130,7 +132,7 @@ func (fp *FloatingPane) ShowURL(ctx context.Context, url string) error { | |||||
| 130 | } | 132 | } | ||
| 131 | 133 | ||||
| 132 | // Hide hides the pane while preserving web session state. | 134 | // Hide hides the pane while preserving web session state. | ||
| 133 | - | func (fp *FloatingPane) Hide(context.Context) { | 135 | + | func (fp *FloatingPane) Hide(_ context.Context) { |
| 134 | fp.mu.Lock() | 136 | fp.mu.Lock() | ||
| 135 | defer fp.mu.Unlock() | 137 | defer fp.mu.Unlock() | ||
| 136 | fp.visible = false | 138 | fp.visible = false | ||
| @@ -164,26 +166,15 @@ func (fp *FloatingPane) Navigate(ctx context.Context, url string) error { | |||||
| 164 | 166 | ||||
| 165 | // Resize recalculates pane dimensions from parent overlay allocation. | 167 | // Resize recalculates pane dimensions from parent overlay allocation. | ||
| 166 | func (fp *FloatingPane) Resize() { | 168 | func (fp *FloatingPane) Resize() { | ||
| 167 | - | fp.mu.RLock() | |||
| 168 | - | parent := fp.parent | |||
| 169 | - | widthPct := fp.widthPct | |||
| 170 | - | heightPct := fp.heightPct | |||
| 171 | - | fallbackWidth := fp.fallbackWidth | |||
| 172 | - | fallbackHeight := fp.fallbackHeight | |||
| 173 | - | fp.mu.RUnlock() | |||
| 174 | - | ||||
| 175 | - | width, height := CalculateOverlayDimensions( | |||
| 176 | - | parent, | |||
| 177 | - | widthPct, | |||
| 178 | - | heightPct, | |||
| 179 | - | fallbackWidth, | |||
| 180 | - | fallbackHeight, | |||
| 181 | - | ) | |||
| 182 | - | ||||
| 183 | fp.mu.Lock() | 169 | fp.mu.Lock() | ||
| 184 | - | fp.calculatedWidth = width | 170 | + | defer fp.mu.Unlock() |
| 185 | - | fp.calculatedHeight = height | 171 | + | fp.calculatedWidth, fp.calculatedHeight = CalculateOverlayDimensions( |
| 186 | - | fp.mu.Unlock() | 172 | + | fp.parent, |
| 173 | + | fp.widthPct, | |||
| 174 | + | fp.heightPct, | |||
| 175 | + | fp.fallbackWidth, | |||
| 176 | + | fp.fallbackHeight, | |||
| 177 | + | ) | |||
| 187 | } | 178 | } | ||
| 188 | 179 | ||||
| 189 | // SetParentOverlay updates the workspace overlay used for sizing calculations. | 180 | // SetParentOverlay updates the workspace overlay used for sizing calculations. | ||
M internal/ui/component/floating_pane_test.go +8 -7
| @@ -1,10 +1,11 @@ | |||
| 1 | - | package component | |
| 1 | + | package component_test | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "sync" | |
| 6 | 6 | "testing" | |
| 7 | 7 | ||
| 8 | + | "github.com/bnema/dumber/internal/ui/component" | |
| 8 | 9 | layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks" | |
| 9 | 10 | "github.com/stretchr/testify/assert" | |
| 10 | 11 | "github.com/stretchr/testify/require" | |
| @@ -16,7 +17,7 @@ func TestFloatingPane_HideShowPreservesState(t *testing.T) { | |||
| 16 | 17 | overlay.EXPECT().GetAllocatedHeight().Return(800).Maybe() | |
| 17 | 18 | ||
| 18 | 19 | navigated := make([]string, 0) | |
| 19 | - | fp := NewFloatingPane(overlay, FloatingPaneOptions{ | |
| 20 | + | fp := component.NewFloatingPane(overlay, component.FloatingPaneOptions{ | |
| 20 | 21 | WidthPct: 0.82, | |
| 21 | 22 | HeightPct: 0.72, | |
| 22 | 23 | FallbackWidth: 1200, | |
| @@ -44,7 +45,7 @@ func TestFloatingPane_ShowURLLoadsTarget(t *testing.T) { | |||
| 44 | 45 | overlay.EXPECT().GetAllocatedWidth().Return(1000).Maybe() | |
| 45 | 46 | overlay.EXPECT().GetAllocatedHeight().Return(700).Maybe() | |
| 46 | 47 | ||
| 47 | - | fp := NewFloatingPane(overlay, FloatingPaneOptions{ | |
| 48 | + | fp := component.NewFloatingPane(overlay, component.FloatingPaneOptions{ | |
| 48 | 49 | WidthPct: 0.82, | |
| 49 | 50 | HeightPct: 0.72, | |
| 50 | 51 | FallbackWidth: 1000, | |
| @@ -63,7 +64,7 @@ func TestFloatingPane_ResizeUsesWorkspacePercentages(t *testing.T) { | |||
| 63 | 64 | overlay.EXPECT().GetAllocatedWidth().Return(1000).Maybe() | |
| 64 | 65 | overlay.EXPECT().GetAllocatedHeight().Return(700).Maybe() | |
| 65 | 66 | ||
| 66 | - | fp := NewFloatingPane(overlay, FloatingPaneOptions{ | |
| 67 | + | fp := component.NewFloatingPane(overlay, component.FloatingPaneOptions{ | |
| 67 | 68 | WidthPct: 0.82, | |
| 68 | 69 | HeightPct: 0.72, | |
| 69 | 70 | FallbackWidth: 1000, | |
| @@ -82,7 +83,7 @@ func TestFloatingPane_FirstToggleOpensBlankAndOmnibox(t *testing.T) { | |||
| 82 | 83 | overlay.EXPECT().GetAllocatedWidth().Return(1200).Maybe() | |
| 83 | 84 | overlay.EXPECT().GetAllocatedHeight().Return(800).Maybe() | |
| 84 | 85 | ||
| 85 | - | fp := NewFloatingPane(overlay, FloatingPaneOptions{ | |
| 86 | + | fp := component.NewFloatingPane(overlay, component.FloatingPaneOptions{ | |
| 86 | 87 | WidthPct: 0.82, | |
| 87 | 88 | HeightPct: 0.72, | |
| 88 | 89 | FallbackWidth: 1200, | |
| @@ -97,7 +98,7 @@ func TestFloatingPane_FirstToggleOpensBlankAndOmnibox(t *testing.T) { | |||
| 97 | 98 | } | |
| 98 | 99 | ||
| 99 | 100 | func TestFloatingPane_SetOmniboxVisible(t *testing.T) { | |
| 100 | - | fp := NewFloatingPane(nil, FloatingPaneOptions{ | |
| 101 | + | fp := component.NewFloatingPane(nil, component.FloatingPaneOptions{ | |
| 101 | 102 | WidthPct: 0.82, | |
| 102 | 103 | HeightPct: 0.72, | |
| 103 | 104 | FallbackWidth: 1200, | |
| @@ -120,7 +121,7 @@ func TestFloatingPane_ConcurrentResizeAndParentSwitch(t *testing.T) { | |||
| 120 | 121 | overlayB.EXPECT().GetAllocatedWidth().Return(1000).Maybe() | |
| 121 | 122 | overlayB.EXPECT().GetAllocatedHeight().Return(700).Maybe() | |
| 122 | 123 | ||
| 123 | - | fp := NewFloatingPane(overlayA, FloatingPaneOptions{ | |
| 124 | + | fp := component.NewFloatingPane(overlayA, component.FloatingPaneOptions{ | |
| 124 | 125 | WidthPct: 0.82, | |
| 125 | 126 | HeightPct: 0.72, | |
| 126 | 127 | FallbackWidth: 1200, | |
| @@ -1,10 +1,11 @@ | |||||
| 1 | - | package component | 1 | + | package component_test |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "sync" | 5 | "sync" | ||
| 6 | "testing" | 6 | "testing" | ||
| 7 | 7 | ||||
| 8 | + | "github.com/bnema/dumber/internal/ui/component" | |||
| 8 | layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks" | 9 | layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks" | ||
| 9 | "github.com/stretchr/testify/assert" | 10 | "github.com/stretchr/testify/assert" | ||
| 10 | "github.com/stretchr/testify/require" | 11 | "github.com/stretchr/testify/require" | ||
| @@ -16,7 +17,7 @@ func TestFloatingPane_HideShowPreservesState(t *testing.T) { | |||||
| 16 | overlay.EXPECT().GetAllocatedHeight().Return(800).Maybe() | 17 | overlay.EXPECT().GetAllocatedHeight().Return(800).Maybe() | ||
| 17 | 18 | ||||
| 18 | navigated := make([]string, 0) | 19 | navigated := make([]string, 0) | ||
| 19 | - | fp := NewFloatingPane(overlay, FloatingPaneOptions{ | 20 | + | fp := component.NewFloatingPane(overlay, component.FloatingPaneOptions{ |
| 20 | WidthPct: 0.82, | 21 | WidthPct: 0.82, | ||
| 21 | HeightPct: 0.72, | 22 | HeightPct: 0.72, | ||
| 22 | FallbackWidth: 1200, | 23 | FallbackWidth: 1200, | ||
| @@ -44,7 +45,7 @@ func TestFloatingPane_ShowURLLoadsTarget(t *testing.T) { | |||||
| 44 | overlay.EXPECT().GetAllocatedWidth().Return(1000).Maybe() | 45 | overlay.EXPECT().GetAllocatedWidth().Return(1000).Maybe() | ||
| 45 | overlay.EXPECT().GetAllocatedHeight().Return(700).Maybe() | 46 | overlay.EXPECT().GetAllocatedHeight().Return(700).Maybe() | ||
| 46 | 47 | ||||
| 47 | - | fp := NewFloatingPane(overlay, FloatingPaneOptions{ | 48 | + | fp := component.NewFloatingPane(overlay, component.FloatingPaneOptions{ |
| 48 | WidthPct: 0.82, | 49 | WidthPct: 0.82, | ||
| 49 | HeightPct: 0.72, | 50 | HeightPct: 0.72, | ||
| 50 | FallbackWidth: 1000, | 51 | FallbackWidth: 1000, | ||
| @@ -63,7 +64,7 @@ func TestFloatingPane_ResizeUsesWorkspacePercentages(t *testing.T) { | |||||
| 63 | overlay.EXPECT().GetAllocatedWidth().Return(1000).Maybe() | 64 | overlay.EXPECT().GetAllocatedWidth().Return(1000).Maybe() | ||
| 64 | overlay.EXPECT().GetAllocatedHeight().Return(700).Maybe() | 65 | overlay.EXPECT().GetAllocatedHeight().Return(700).Maybe() | ||
| 65 | 66 | ||||
| 66 | - | fp := NewFloatingPane(overlay, FloatingPaneOptions{ | 67 | + | fp := component.NewFloatingPane(overlay, component.FloatingPaneOptions{ |
| 67 | WidthPct: 0.82, | 68 | WidthPct: 0.82, | ||
| 68 | HeightPct: 0.72, | 69 | HeightPct: 0.72, | ||
| 69 | FallbackWidth: 1000, | 70 | FallbackWidth: 1000, | ||
| @@ -82,7 +83,7 @@ func TestFloatingPane_FirstToggleOpensBlankAndOmnibox(t *testing.T) { | |||||
| 82 | overlay.EXPECT().GetAllocatedWidth().Return(1200).Maybe() | 83 | overlay.EXPECT().GetAllocatedWidth().Return(1200).Maybe() | ||
| 83 | overlay.EXPECT().GetAllocatedHeight().Return(800).Maybe() | 84 | overlay.EXPECT().GetAllocatedHeight().Return(800).Maybe() | ||
| 84 | 85 | ||||
| 85 | - | fp := NewFloatingPane(overlay, FloatingPaneOptions{ | 86 | + | fp := component.NewFloatingPane(overlay, component.FloatingPaneOptions{ |
| 86 | WidthPct: 0.82, | 87 | WidthPct: 0.82, | ||
| 87 | HeightPct: 0.72, | 88 | HeightPct: 0.72, | ||
| 88 | FallbackWidth: 1200, | 89 | FallbackWidth: 1200, | ||
| @@ -97,7 +98,7 @@ func TestFloatingPane_FirstToggleOpensBlankAndOmnibox(t *testing.T) { | |||||
| 97 | } | 98 | } | ||
| 98 | 99 | ||||
| 99 | func TestFloatingPane_SetOmniboxVisible(t *testing.T) { | 100 | func TestFloatingPane_SetOmniboxVisible(t *testing.T) { | ||
| 100 | - | fp := NewFloatingPane(nil, FloatingPaneOptions{ | 101 | + | fp := component.NewFloatingPane(nil, component.FloatingPaneOptions{ |
| 101 | WidthPct: 0.82, | 102 | WidthPct: 0.82, | ||
| 102 | HeightPct: 0.72, | 103 | HeightPct: 0.72, | ||
| 103 | FallbackWidth: 1200, | 104 | FallbackWidth: 1200, | ||
| @@ -120,7 +121,7 @@ func TestFloatingPane_ConcurrentResizeAndParentSwitch(t *testing.T) { | |||||
| 120 | overlayB.EXPECT().GetAllocatedWidth().Return(1000).Maybe() | 121 | overlayB.EXPECT().GetAllocatedWidth().Return(1000).Maybe() | ||
| 121 | overlayB.EXPECT().GetAllocatedHeight().Return(700).Maybe() | 122 | overlayB.EXPECT().GetAllocatedHeight().Return(700).Maybe() | ||
| 122 | 123 | ||||
| 123 | - | fp := NewFloatingPane(overlayA, FloatingPaneOptions{ | 124 | + | fp := component.NewFloatingPane(overlayA, component.FloatingPaneOptions{ |
| 124 | WidthPct: 0.82, | 125 | WidthPct: 0.82, | ||
| 125 | HeightPct: 0.72, | 126 | HeightPct: 0.72, | ||
| 126 | FallbackWidth: 1200, | 127 | FallbackWidth: 1200, | ||
M internal/ui/component/workspace_view_test.go +2 -0
| @@ -259,6 +259,8 @@ func TestSetWorkspace_SinglePane_CreatesPaneView(t *testing.T) { | |||
| 259 | 259 | } | |
| 260 | 260 | ||
| 261 | 261 | func TestWorkspaceView_HoverFocusLockState(t *testing.T) { | |
| 262 | + | // Intentional zero-value check: hoverFocusLock is an atomic.Bool and should | |
| 263 | + | // be safe with its language-level zero value before constructor wiring. | |
| 262 | 264 | var wv component.WorkspaceView | |
| 263 | 265 | ||
| 264 | 266 | assert.False(t, wv.IsHoverFocusLocked()) | |
| @@ -259,6 +259,8 @@ func TestSetWorkspace_SinglePane_CreatesPaneView(t *testing.T) { | |||||
| 259 | } | 259 | } | ||
| 260 | 260 | ||||
| 261 | func TestWorkspaceView_HoverFocusLockState(t *testing.T) { | 261 | func TestWorkspaceView_HoverFocusLockState(t *testing.T) { | ||
| 262 | + | // Intentional zero-value check: hoverFocusLock is an atomic.Bool and should | |||
| 263 | + | // be safe with its language-level zero value before constructor wiring. | |||
| 262 | var wv component.WorkspaceView | 264 | var wv component.WorkspaceView | ||
| 263 | 265 | ||||
| 264 | assert.False(t, wv.IsHoverFocusLocked()) | 266 | assert.False(t, wv.IsHoverFocusLocked()) | ||
M internal/ui/coordinator/content.go +35 -10
| @@ -1063,22 +1063,47 @@ func (c *ContentCoordinator) PreloadCachedFavicon(ctx context.Context, paneID en | |||
| 1063 | 1063 | if c.faviconAdapter == nil || uri == "" { | |
| 1064 | 1064 | return | |
| 1065 | 1065 | } | |
| 1066 | + | select { | |
| 1067 | + | case <-ctx.Done(): | |
| 1068 | + | return | |
| 1069 | + | default: | |
| 1070 | + | } | |
| 1066 | 1071 | ||
| 1067 | 1072 | // Check memory and disk cache (no external fetch) | |
| 1068 | 1073 | texture := c.faviconAdapter.PreloadFromCache(uri) | |
| 1074 | + | select { | |
| 1075 | + | case <-ctx.Done(): | |
| 1076 | + | return | |
| 1077 | + | default: | |
| 1078 | + | } | |
| 1069 | 1079 | ||
| 1070 | - | // Update stacked pane favicon if applicable. | |
| 1071 | - | // A nil texture triggers the default icon fallback, which avoids stale favicons. | |
| 1072 | - | _, wsView := c.getActiveWS() | |
| 1073 | - | if wsView != nil { | |
| 1080 | + | // Update stacked pane favicon on GTK main loop. | |
| 1081 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 1082 | + | select { | |
| 1083 | + | case <-ctx.Done(): | |
| 1084 | + | return false | |
| 1085 | + | default: | |
| 1086 | + | } | |
| 1087 | + | if c.getActiveWS == nil { | |
| 1088 | + | return false | |
| 1089 | + | } | |
| 1090 | + | // A nil texture triggers the default icon fallback, which avoids stale favicons. | |
| 1091 | + | _, wsView := c.getActiveWS() | |
| 1092 | + | if wsView == nil { | |
| 1093 | + | return false | |
| 1094 | + | } | |
| 1074 | 1095 | tr := wsView.TreeRenderer() | |
| 1075 | - | if tr != nil { | |
| 1076 | - | stackedView := tr.GetStackedViewForPane(string(paneID)) | |
| 1077 | - | if stackedView != nil { | |
| 1078 | - | c.updateStackedPaneFavicon(ctx, stackedView, paneID, texture) | |
| 1079 | - | } | |
| 1096 | + | if tr == nil { | |
| 1097 | + | return false | |
| 1080 | 1098 | } | |
| 1081 | - | } | |
| 1099 | + | stackedView := tr.GetStackedViewForPane(string(paneID)) | |
| 1100 | + | if stackedView == nil { | |
| 1101 | + | return false | |
| 1102 | + | } | |
| 1103 | + | c.updateStackedPaneFavicon(ctx, stackedView, paneID, texture) | |
| 1104 | + | return false | |
| 1105 | + | }) | |
| 1106 | + | glib.IdleAdd(&cb, 0) | |
| 1082 | 1107 | } | |
| 1083 | 1108 | ||
| 1084 | 1109 | // onLoadCommitted re-applies zoom when page content starts loading and records history. | |
| @@ -1063,22 +1063,47 @@ func (c *ContentCoordinator) PreloadCachedFavicon(ctx context.Context, paneID en | |||||
| 1063 | if c.faviconAdapter == nil || uri == "" { | 1063 | if c.faviconAdapter == nil || uri == "" { | ||
| 1064 | return | 1064 | return | ||
| 1065 | } | 1065 | } | ||
| 1066 | + | select { | |||
| 1067 | + | case <-ctx.Done(): | |||
| 1068 | + | return | |||
| 1069 | + | default: | |||
| 1070 | + | } | |||
| 1066 | 1071 | ||||
| 1067 | // Check memory and disk cache (no external fetch) | 1072 | // Check memory and disk cache (no external fetch) | ||
| 1068 | texture := c.faviconAdapter.PreloadFromCache(uri) | 1073 | texture := c.faviconAdapter.PreloadFromCache(uri) | ||
| 1074 | + | select { | |||
| 1075 | + | case <-ctx.Done(): | |||
| 1076 | + | return | |||
| 1077 | + | default: | |||
| 1078 | + | } | |||
| 1069 | 1079 | ||||
| 1070 | - | // Update stacked pane favicon if applicable. | 1080 | + | // Update stacked pane favicon on GTK main loop. |
| 1071 | - | // A nil texture triggers the default icon fallback, which avoids stale favicons. | 1081 | + | cb := glib.SourceFunc(func(_ uintptr) bool { |
| 1072 | - | _, wsView := c.getActiveWS() | 1082 | + | select { |
| 1073 | - | if wsView != nil { | 1083 | + | case <-ctx.Done(): |
| 1084 | + | return false | |||
| 1085 | + | default: | |||
| 1086 | + | } | |||
| 1087 | + | if c.getActiveWS == nil { | |||
| 1088 | + | return false | |||
| 1089 | + | } | |||
| 1090 | + | // A nil texture triggers the default icon fallback, which avoids stale favicons. | |||
| 1091 | + | _, wsView := c.getActiveWS() | |||
| 1092 | + | if wsView == nil { | |||
| 1093 | + | return false | |||
| 1094 | + | } | |||
| 1074 | tr := wsView.TreeRenderer() | 1095 | tr := wsView.TreeRenderer() | ||
| 1075 | - | if tr != nil { | 1096 | + | if tr == nil { |
| 1076 | - | stackedView := tr.GetStackedViewForPane(string(paneID)) | 1097 | + | return false |
| 1077 | - | if stackedView != nil { | |||
| 1078 | - | c.updateStackedPaneFavicon(ctx, stackedView, paneID, texture) | |||
| 1079 | - | } | |||
| 1080 | } | 1098 | } | ||
| 1081 | - | } | 1099 | + | stackedView := tr.GetStackedViewForPane(string(paneID)) |
| 1100 | + | if stackedView == nil { | |||
| 1101 | + | return false | |||
| 1102 | + | } | |||
| 1103 | + | c.updateStackedPaneFavicon(ctx, stackedView, paneID, texture) | |||
| 1104 | + | return false | |||
| 1105 | + | }) | |||
| 1106 | + | glib.IdleAdd(&cb, 0) | |||
| 1082 | } | 1107 | } | ||
| 1083 | 1108 | ||||
| 1084 | // onLoadCommitted re-applies zoom when page content starts loading and records history. | 1109 | // onLoadCommitted re-applies zoom when page content starts loading and records history. | ||
M internal/ui/coordinator/content_active_override_test.go +23 -0
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | 8 | ||
| 9 | 9 | "github.com/bnema/dumber/internal/domain/entity" | |
| 10 | 10 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 11 | + | "github.com/bnema/dumber/internal/ui/component" | |
| 11 | 12 | ) | |
| 12 | 13 | ||
| 13 | 14 | func TestContentCoordinator_ActiveWebView_UsesOverride(t *testing.T) { | |
| @@ -42,3 +43,25 @@ func TestContentCoordinator_ActiveWebView_ClearOverrideFallsBack(t *testing.T) { | |||
| 42 | 43 | assert.Nil(t, c.ActiveWebView(context.Background())) | |
| 43 | 44 | assert.Equal(t, entity.PaneID(""), c.ActivePaneID(context.Background())) | |
| 44 | 45 | } | |
| 46 | + | ||
| 47 | + | func TestContentCoordinator_ActiveWebView_ClearOverrideFallsBackToWorkspace(t *testing.T) { | |
| 48 | + | ctx := context.Background() | |
| 49 | + | mainWV := &webkit.WebView{} | |
| 50 | + | mainPane := entity.NewPane(entity.PaneID("main-pane")) | |
| 51 | + | ws := entity.NewWorkspace(entity.WorkspaceID("ws-1"), mainPane) | |
| 52 | + | ||
| 53 | + | c := &ContentCoordinator{ | |
| 54 | + | webViews: map[entity.PaneID]*webkit.WebView{ | |
| 55 | + | mainPane.ID: mainWV, | |
| 56 | + | }, | |
| 57 | + | getActiveWS: func() (*entity.Workspace, *component.WorkspaceView) { | |
| 58 | + | return ws, nil | |
| 59 | + | }, | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | c.SetActivePaneOverride("floating-pane") | |
| 63 | + | c.ClearActivePaneOverride() | |
| 64 | + | ||
| 65 | + | assert.Equal(t, mainWV, c.ActiveWebView(ctx)) | |
| 66 | + | assert.Equal(t, ws.ActivePaneID, c.ActivePaneID(ctx)) | |
| 67 | + | } | |
| @@ -8,6 +8,7 @@ import ( | |||||
| 8 | 8 | ||||
| 9 | "github.com/bnema/dumber/internal/domain/entity" | 9 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 10 | "github.com/bnema/dumber/internal/infrastructure/webkit" | 10 | "github.com/bnema/dumber/internal/infrastructure/webkit" | ||
| 11 | + | "github.com/bnema/dumber/internal/ui/component" | |||
| 11 | ) | 12 | ) | ||
| 12 | 13 | ||||
| 13 | func TestContentCoordinator_ActiveWebView_UsesOverride(t *testing.T) { | 14 | func TestContentCoordinator_ActiveWebView_UsesOverride(t *testing.T) { | ||
| @@ -42,3 +43,25 @@ func TestContentCoordinator_ActiveWebView_ClearOverrideFallsBack(t *testing.T) { | |||||
| 42 | assert.Nil(t, c.ActiveWebView(context.Background())) | 43 | assert.Nil(t, c.ActiveWebView(context.Background())) | ||
| 43 | assert.Equal(t, entity.PaneID(""), c.ActivePaneID(context.Background())) | 44 | assert.Equal(t, entity.PaneID(""), c.ActivePaneID(context.Background())) | ||
| 44 | } | 45 | } | ||
| 46 | + | ||||
| 47 | + | func TestContentCoordinator_ActiveWebView_ClearOverrideFallsBackToWorkspace(t *testing.T) { | |||
| 48 | + | ctx := context.Background() | |||
| 49 | + | mainWV := &webkit.WebView{} | |||
| 50 | + | mainPane := entity.NewPane(entity.PaneID("main-pane")) | |||
| 51 | + | ws := entity.NewWorkspace(entity.WorkspaceID("ws-1"), mainPane) | |||
| 52 | + | ||||
| 53 | + | c := &ContentCoordinator{ | |||
| 54 | + | webViews: map[entity.PaneID]*webkit.WebView{ | |||
| 55 | + | mainPane.ID: mainWV, | |||
| 56 | + | }, | |||
| 57 | + | getActiveWS: func() (*entity.Workspace, *component.WorkspaceView) { | |||
| 58 | + | return ws, nil | |||
| 59 | + | }, | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | c.SetActivePaneOverride("floating-pane") | |||
| 63 | + | c.ClearActivePaneOverride() | |||
| 64 | + | ||||
| 65 | + | assert.Equal(t, mainWV, c.ActiveWebView(ctx)) | |||
| 66 | + | assert.Equal(t, ws.ActivePaneID, c.ActivePaneID(ctx)) | |||
| 67 | + | } | |||
M internal/ui/coordinator/navigation.go +8 -1
| @@ -3,6 +3,7 @@ package coordinator | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | + | "time" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/dumber/internal/application/usecase" | |
| 8 | 9 | "github.com/bnema/dumber/internal/domain/entity" | |
| @@ -24,6 +25,8 @@ type NavigationCoordinator struct { | |||
| 24 | 25 | omniboxProvider OmniboxProvider | |
| 25 | 26 | } | |
| 26 | 27 | ||
| 28 | + | const faviconPreloadTimeout = 300 * time.Millisecond | |
| 29 | + | ||
| 27 | 30 | // NewNavigationCoordinator creates a new NavigationCoordinator. | |
| 28 | 31 | func NewNavigationCoordinator( | |
| 29 | 32 | ctx context.Context, | |
| @@ -74,7 +77,11 @@ func (c *NavigationCoordinator) Navigate(ctx context.Context, url string) error | |||
| 74 | 77 | // Track original URL for cross-domain redirect favicon caching | |
| 75 | 78 | c.contentCoord.SetNavigationOrigin(activePaneID, url) | |
| 76 | 79 | // Pre-load cached favicon asynchronously (don't block navigation start) | |
| 77 | - | go c.contentCoord.PreloadCachedFavicon(ctx, activePaneID, url) | |
| 80 | + | preloadCtx, cancelPreload := context.WithTimeout(ctx, faviconPreloadTimeout) | |
| 81 | + | go func() { | |
| 82 | + | defer cancelPreload() | |
| 83 | + | c.contentCoord.PreloadCachedFavicon(preloadCtx, activePaneID, url) | |
| 84 | + | }() | |
| 78 | 85 | } | |
| 79 | 86 | ||
| 80 | 87 | // Use NavigateUseCase which handles history + zoom | |
| @@ -3,6 +3,7 @@ package coordinator | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | + | "time" | |||
| 6 | 7 | ||||
| 7 | "github.com/bnema/dumber/internal/application/usecase" | 8 | "github.com/bnema/dumber/internal/application/usecase" | ||
| 8 | "github.com/bnema/dumber/internal/domain/entity" | 9 | "github.com/bnema/dumber/internal/domain/entity" | ||
| @@ -24,6 +25,8 @@ type NavigationCoordinator struct { | |||||
| 24 | omniboxProvider OmniboxProvider | 25 | omniboxProvider OmniboxProvider | ||
| 25 | } | 26 | } | ||
| 26 | 27 | ||||
| 28 | + | const faviconPreloadTimeout = 300 * time.Millisecond | |||
| 29 | + | ||||
| 27 | // NewNavigationCoordinator creates a new NavigationCoordinator. | 30 | // NewNavigationCoordinator creates a new NavigationCoordinator. | ||
| 28 | func NewNavigationCoordinator( | 31 | func NewNavigationCoordinator( | ||
| 29 | ctx context.Context, | 32 | ctx context.Context, | ||
| @@ -74,7 +77,11 @@ func (c *NavigationCoordinator) Navigate(ctx context.Context, url string) error | |||||
| 74 | // Track original URL for cross-domain redirect favicon caching | 77 | // Track original URL for cross-domain redirect favicon caching | ||
| 75 | c.contentCoord.SetNavigationOrigin(activePaneID, url) | 78 | c.contentCoord.SetNavigationOrigin(activePaneID, url) | ||
| 76 | // Pre-load cached favicon asynchronously (don't block navigation start) | 79 | // Pre-load cached favicon asynchronously (don't block navigation start) | ||
| 77 | - | go c.contentCoord.PreloadCachedFavicon(ctx, activePaneID, url) | 80 | + | preloadCtx, cancelPreload := context.WithTimeout(ctx, faviconPreloadTimeout) |
| 81 | + | go func() { | |||
| 82 | + | defer cancelPreload() | |||
| 83 | + | c.contentCoord.PreloadCachedFavicon(preloadCtx, activePaneID, url) | |||
| 84 | + | }() | |||
| 78 | } | 85 | } | ||
| 79 | 86 | ||||
| 80 | // Use NavigateUseCase which handles history + zoom | 87 | // Use NavigateUseCase which handles history + zoom | ||
M internal/ui/input/global_shortcuts.go +7 -1
| @@ -3,6 +3,7 @@ package input | |||
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | 5 | "context" | |
| 6 | + | "fmt" | |
| 6 | 7 | "strings" | |
| 7 | 8 | ||
| 8 | 9 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| @@ -100,6 +101,7 @@ func NewGlobalShortcutHandler( | |||
| 100 | 101 | // Map action names to action constants for global shortcuts | |
| 101 | 102 | actionMap := map[string]Action{ | |
| 102 | 103 | "toggle_floating_pane": ActionToggleFloatingPane, | |
| 104 | + | "toggle-floating-pane": ActionToggleFloatingPane, | |
| 103 | 105 | "consume_or_expel_left": ActionConsumeOrExpelLeft, | |
| 104 | 106 | "consume_or_expel_right": ActionConsumeOrExpelRight, | |
| 105 | 107 | "consume_or_expel_up": ActionConsumeOrExpelUp, | |
| @@ -238,6 +240,10 @@ func formatBinding(binding KeyBinding) string { | |||
| 238 | 240 | if binding.Modifiers&ModAlt != 0 { | |
| 239 | 241 | parts = append(parts, "alt") | |
| 240 | 242 | } | |
| 241 | - | parts = append(parts, strings.ToLower(gdk.KeyvalName(binding.Keyval))) | |
| 243 | + | keyName := gdk.KeyvalName(binding.Keyval) | |
| 244 | + | if keyName == "" { | |
| 245 | + | keyName = fmt.Sprintf("0x%x", binding.Keyval) | |
| 246 | + | } | |
| 247 | + | parts = append(parts, strings.ToLower(keyName)) | |
| 242 | 248 | return strings.Join(parts, "+") | |
| 243 | 249 | } | |
| @@ -3,6 +3,7 @@ package input | |||||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | "context" | 5 | "context" | ||
| 6 | + | "fmt" | |||
| 6 | "strings" | 7 | "strings" | ||
| 7 | 8 | ||||
| 8 | "github.com/bnema/dumber/internal/infrastructure/config" | 9 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| @@ -100,6 +101,7 @@ func NewGlobalShortcutHandler( | |||||
| 100 | // Map action names to action constants for global shortcuts | 101 | // Map action names to action constants for global shortcuts | ||
| 101 | actionMap := map[string]Action{ | 102 | actionMap := map[string]Action{ | ||
| 102 | "toggle_floating_pane": ActionToggleFloatingPane, | 103 | "toggle_floating_pane": ActionToggleFloatingPane, | ||
| 104 | + | "toggle-floating-pane": ActionToggleFloatingPane, | |||
| 103 | "consume_or_expel_left": ActionConsumeOrExpelLeft, | 105 | "consume_or_expel_left": ActionConsumeOrExpelLeft, | ||
| 104 | "consume_or_expel_right": ActionConsumeOrExpelRight, | 106 | "consume_or_expel_right": ActionConsumeOrExpelRight, | ||
| 105 | "consume_or_expel_up": ActionConsumeOrExpelUp, | 107 | "consume_or_expel_up": ActionConsumeOrExpelUp, | ||
| @@ -238,6 +240,10 @@ func formatBinding(binding KeyBinding) string { | |||||
| 238 | if binding.Modifiers&ModAlt != 0 { | 240 | if binding.Modifiers&ModAlt != 0 { | ||
| 239 | parts = append(parts, "alt") | 241 | parts = append(parts, "alt") | ||
| 240 | } | 242 | } | ||
| 241 | - | parts = append(parts, strings.ToLower(gdk.KeyvalName(binding.Keyval))) | 243 | + | keyName := gdk.KeyvalName(binding.Keyval) |
| 244 | + | if keyName == "" { | |||
| 245 | + | keyName = fmt.Sprintf("0x%x", binding.Keyval) | |||
| 246 | + | } | |||
| 247 | + | parts = append(parts, strings.ToLower(keyName)) | |||
| 242 | return strings.Join(parts, "+") | 248 | return strings.Join(parts, "+") | ||
| 243 | } | 249 | } | ||
M internal/ui/input/shortcuts.go +4 -0
| @@ -334,6 +334,7 @@ func (s *ShortcutSet) registerConfiguredShortcuts(cfg *config.WorkspaceConfig) { | |||
| 334 | 334 | // Map action names to action constants | |
| 335 | 335 | actionMap := map[string]Action{ | |
| 336 | 336 | "toggle_floating_pane": ActionToggleFloatingPane, | |
| 337 | + | "toggle-floating-pane": ActionToggleFloatingPane, | |
| 337 | 338 | "close_pane": ActionClosePane, | |
| 338 | 339 | "next_tab": ActionNextTab, | |
| 339 | 340 | "previous_tab": ActionPreviousTab, | |
| @@ -442,6 +443,8 @@ func reserveGlobalOnlyShortcutBindings(occupied map[KeyBinding]Action) { | |||
| 442 | 443 | ActionSwitchTabIndex9, | |
| 443 | 444 | } | |
| 444 | 445 | ||
| 446 | + | // Assumes gdk.KEY_1..gdk.KEY_9 are contiguous (ASCII '1'..'9'). | |
| 447 | + | // If that ever changes on a platform, map digits explicitly. | |
| 445 | 448 | for i, action := range tabIndexActions { | |
| 446 | 449 | binding := KeyBinding{Keyval: uint(gdk.KEY_1) + uint(i), Modifiers: ModAlt} | |
| 447 | 450 | if _, exists := occupied[binding]; exists { | |
| @@ -541,6 +544,7 @@ func (s *ShortcutSet) buildModeShortcuts(ctx context.Context, bindings map[strin | |||
| 541 | 544 | ||
| 542 | 545 | var configActionToAction = map[string]Action{ | |
| 543 | 546 | "toggle_floating_pane": ActionToggleFloatingPane, | |
| 547 | + | "toggle-floating-pane": ActionToggleFloatingPane, | |
| 544 | 548 | ||
| 545 | 549 | // Tab actions | |
| 546 | 550 | "new-tab": ActionNewTab, | |
| @@ -334,6 +334,7 @@ func (s *ShortcutSet) registerConfiguredShortcuts(cfg *config.WorkspaceConfig) { | |||||
| 334 | // Map action names to action constants | 334 | // Map action names to action constants | ||
| 335 | actionMap := map[string]Action{ | 335 | actionMap := map[string]Action{ | ||
| 336 | "toggle_floating_pane": ActionToggleFloatingPane, | 336 | "toggle_floating_pane": ActionToggleFloatingPane, | ||
| 337 | + | "toggle-floating-pane": ActionToggleFloatingPane, | |||
| 337 | "close_pane": ActionClosePane, | 338 | "close_pane": ActionClosePane, | ||
| 338 | "next_tab": ActionNextTab, | 339 | "next_tab": ActionNextTab, | ||
| 339 | "previous_tab": ActionPreviousTab, | 340 | "previous_tab": ActionPreviousTab, | ||
| @@ -442,6 +443,8 @@ func reserveGlobalOnlyShortcutBindings(occupied map[KeyBinding]Action) { | |||||
| 442 | ActionSwitchTabIndex9, | 443 | ActionSwitchTabIndex9, | ||
| 443 | } | 444 | } | ||
| 444 | 445 | ||||
| 446 | + | // Assumes gdk.KEY_1..gdk.KEY_9 are contiguous (ASCII '1'..'9'). | |||
| 447 | + | // If that ever changes on a platform, map digits explicitly. | |||
| 445 | for i, action := range tabIndexActions { | 448 | for i, action := range tabIndexActions { | ||
| 446 | binding := KeyBinding{Keyval: uint(gdk.KEY_1) + uint(i), Modifiers: ModAlt} | 449 | binding := KeyBinding{Keyval: uint(gdk.KEY_1) + uint(i), Modifiers: ModAlt} | ||
| 447 | if _, exists := occupied[binding]; exists { | 450 | if _, exists := occupied[binding]; exists { | ||
| @@ -541,6 +544,7 @@ func (s *ShortcutSet) buildModeShortcuts(ctx context.Context, bindings map[strin | |||||
| 541 | 544 | ||||
| 542 | var configActionToAction = map[string]Action{ | 545 | var configActionToAction = map[string]Action{ | ||
| 543 | "toggle_floating_pane": ActionToggleFloatingPane, | 546 | "toggle_floating_pane": ActionToggleFloatingPane, | ||
| 547 | + | "toggle-floating-pane": ActionToggleFloatingPane, | |||
| 544 | 548 | ||||
| 545 | // Tab actions | 549 | // Tab actions | ||
| 546 | "new-tab": ActionNewTab, | 550 | "new-tab": ActionNewTab, | ||
M internal/ui/input/shortcuts_test.go +7 -0
| @@ -382,3 +382,10 @@ func TestMapConfigAction_ToggleFloatingPane(t *testing.T) { | |||
| 382 | 382 | t.Fatalf("mapConfigAction(toggle_floating_pane) = %s, want %s", action, ActionToggleFloatingPane) | |
| 383 | 383 | } | |
| 384 | 384 | } | |
| 385 | + | ||
| 386 | + | func TestMapConfigAction_ToggleFloatingPaneHyphenAlias(t *testing.T) { | |
| 387 | + | action := mapConfigAction("toggle-floating-pane") | |
| 388 | + | if action != ActionToggleFloatingPane { | |
| 389 | + | t.Fatalf("mapConfigAction(toggle-floating-pane) = %s, want %s", action, ActionToggleFloatingPane) | |
| 390 | + | } | |
| 391 | + | } | |
| @@ -382,3 +382,10 @@ func TestMapConfigAction_ToggleFloatingPane(t *testing.T) { | |||||
| 382 | t.Fatalf("mapConfigAction(toggle_floating_pane) = %s, want %s", action, ActionToggleFloatingPane) | 382 | t.Fatalf("mapConfigAction(toggle_floating_pane) = %s, want %s", action, ActionToggleFloatingPane) | ||
| 383 | } | 383 | } | ||
| 384 | } | 384 | } | ||
| 385 | + | ||||
| 386 | + | func TestMapConfigAction_ToggleFloatingPaneHyphenAlias(t *testing.T) { | |||
| 387 | + | action := mapConfigAction("toggle-floating-pane") | |||
| 388 | + | if action != ActionToggleFloatingPane { | |||
| 389 | + | t.Fatalf("mapConfigAction(toggle-floating-pane) = %s, want %s", action, ActionToggleFloatingPane) | |||
| 390 | + | } | |||
| 391 | + | } | |||
M internal/ui/theme/css.go +0 -4
| @@ -997,10 +997,6 @@ func generateFloatingPaneCSS(p Palette) string { | |||
| 997 | 997 | border-color: var(--accent); | |
| 998 | 998 | } | |
| 999 | 999 | ||
| 1000 | - | .single-pane .floating-pane-container.pane-active { | |
| 1001 | - | border-color: var(--accent); | |
| 1002 | - | } | |
| 1003 | - | ||
| 1004 | 1000 | .floating-pane-omnibox { | |
| 1005 | 1001 | background-color: shade(var(--surface-variant), 1.08); | |
| 1006 | 1002 | border-bottom: 0.0625em solid var(--border); | |
| @@ -997,10 +997,6 @@ func generateFloatingPaneCSS(p Palette) string { | |||||
| 997 | border-color: var(--accent); | 997 | border-color: var(--accent); | ||
| 998 | } | 998 | } | ||
| 999 | 999 | ||||
| 1000 | - | .single-pane .floating-pane-container.pane-active { | |||
| 1001 | - | border-color: var(--accent); | |||
| 1002 | - | } | |||
| 1003 | - | ||||
| 1004 | .floating-pane-omnibox { | 1000 | .floating-pane-omnibox { | ||
| 1005 | background-color: shade(var(--surface-variant), 1.08); | 1001 | background-color: shade(var(--surface-variant), 1.08); | ||
| 1006 | border-bottom: 0.0625em solid var(--border); | 1002 | border-bottom: 0.0625em solid var(--border); | ||