feat(keyboard): implement zoom actions (in/out/reset) with omnibox integration
- commit
- 4622b8cf594b4dfae003605ca4d109c75eb59308
- author
- brice · · 2025-12-15 18:41 UTC
- committer
- brice · · 2025-12-15 22:45 UTC
- parent
- f163db7
- signature
- unsigned
3 files · +82 -4
M internal/ui/app.go +2 -0
| @@ -196,6 +196,7 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 196 | 196 | Shortcuts: a.deps.Config.SearchShortcuts, | |
| 197 | 197 | DefaultSearch: a.deps.Config.DefaultSearchEngine, | |
| 198 | 198 | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | |
| 199 | + | UIScale: a.deps.Config.DefaultUIScale, | |
| 199 | 200 | }) | |
| 200 | 201 | if a.omnibox != nil { | |
| 201 | 202 | a.omnibox.SetOnNavigate(func(url string) { | |
| @@ -299,6 +300,7 @@ func (a *App) initCoordinators(ctx context.Context) { | |||
| 299 | 300 | a.tabCoord, | |
| 300 | 301 | a.wsCoord, | |
| 301 | 302 | a.navCoord, | |
| 303 | + | a.deps.ZoomUC, | |
| 302 | 304 | ) | |
| 303 | 305 | a.kbDispatcher.SetOnQuit(a.Quit) | |
| 304 | 306 | ||
| @@ -196,6 +196,7 @@ func (a *App) onActivate(ctx context.Context) { | |||||
| 196 | Shortcuts: a.deps.Config.SearchShortcuts, | 196 | Shortcuts: a.deps.Config.SearchShortcuts, | ||
| 197 | DefaultSearch: a.deps.Config.DefaultSearchEngine, | 197 | DefaultSearch: a.deps.Config.DefaultSearchEngine, | ||
| 198 | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | 198 | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | ||
| 199 | + | UIScale: a.deps.Config.DefaultUIScale, | |||
| 199 | }) | 200 | }) | ||
| 200 | if a.omnibox != nil { | 201 | if a.omnibox != nil { | ||
| 201 | a.omnibox.SetOnNavigate(func(url string) { | 202 | a.omnibox.SetOnNavigate(func(url string) { | ||
| @@ -299,6 +300,7 @@ func (a *App) initCoordinators(ctx context.Context) { | |||||
| 299 | a.tabCoord, | 300 | a.tabCoord, | ||
| 300 | a.wsCoord, | 301 | a.wsCoord, | ||
| 301 | a.navCoord, | 302 | a.navCoord, | ||
| 303 | + | a.deps.ZoomUC, | |||
| 302 | ) | 304 | ) | ||
| 303 | a.kbDispatcher.SetOnQuit(a.Quit) | 305 | a.kbDispatcher.SetOnQuit(a.Quit) | ||
| 304 | 306 | ||||
M internal/ui/coordinator/navigation.go +12 -0
| @@ -204,3 +204,15 @@ func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID e | |||
| 204 | 204 | log.Warn().Err(err).Str("url", url).Msg("failed to update history title") | |
| 205 | 205 | } | |
| 206 | 206 | } | |
| 207 | + | ||
| 208 | + | // ActiveWebView returns the WebView for the active pane (for zoom operations). | |
| 209 | + | func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebView { | |
| 210 | + | return c.contentCoord.ActiveWebView(ctx) | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | // NotifyZoomChanged updates the omnibox zoom indicator. | |
| 214 | + | func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) { | |
| 215 | + | if c.omnibox != nil { | |
| 216 | + | c.omnibox.UpdateZoomIndicator(factor) | |
| 217 | + | } | |
| 218 | + | } | |
| @@ -204,3 +204,15 @@ func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID e | |||||
| 204 | log.Warn().Err(err).Str("url", url).Msg("failed to update history title") | 204 | log.Warn().Err(err).Str("url", url).Msg("failed to update history title") | ||
| 205 | } | 205 | } | ||
| 206 | } | 206 | } | ||
| 207 | + | ||||
| 208 | + | // ActiveWebView returns the WebView for the active pane (for zoom operations). | |||
| 209 | + | func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebView { | |||
| 210 | + | return c.contentCoord.ActiveWebView(ctx) | |||
| 211 | + | } | |||
| 212 | + | ||||
| 213 | + | // NotifyZoomChanged updates the omnibox zoom indicator. | |||
| 214 | + | func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) { | |||
| 215 | + | if c.omnibox != nil { | |||
| 216 | + | c.omnibox.UpdateZoomIndicator(factor) | |||
| 217 | + | } | |||
| 218 | + | } | |||
M internal/ui/dispatcher/keyboard.go +68 -4
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | ||
| 6 | 6 | "github.com/bnema/dumber/internal/application/usecase" | |
| 7 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 7 | 8 | "github.com/bnema/dumber/internal/logging" | |
| 8 | 9 | "github.com/bnema/dumber/internal/ui/coordinator" | |
| 9 | 10 | "github.com/bnema/dumber/internal/ui/input" | |
| @@ -14,6 +15,7 @@ type KeyboardDispatcher struct { | |||
| 14 | 15 | tabCoord *coordinator.TabCoordinator | |
| 15 | 16 | wsCoord *coordinator.WorkspaceCoordinator | |
| 16 | 17 | navCoord *coordinator.NavigationCoordinator | |
| 18 | + | zoomUC *usecase.ManageZoomUseCase | |
| 17 | 19 | onQuit func() | |
| 18 | 20 | } | |
| 19 | 21 | ||
| @@ -23,6 +25,7 @@ func NewKeyboardDispatcher( | |||
| 23 | 25 | tabCoord *coordinator.TabCoordinator, | |
| 24 | 26 | wsCoord *coordinator.WorkspaceCoordinator, | |
| 25 | 27 | navCoord *coordinator.NavigationCoordinator, | |
| 28 | + | zoomUC *usecase.ManageZoomUseCase, | |
| 26 | 29 | ) *KeyboardDispatcher { | |
| 27 | 30 | log := logging.FromContext(ctx) | |
| 28 | 31 | log.Debug().Msg("creating keyboard dispatcher") | |
| @@ -31,6 +34,7 @@ func NewKeyboardDispatcher( | |||
| 31 | 34 | tabCoord: tabCoord, | |
| 32 | 35 | wsCoord: wsCoord, | |
| 33 | 36 | navCoord: navCoord, | |
| 37 | + | zoomUC: zoomUC, | |
| 34 | 38 | } | |
| 35 | 39 | } | |
| 36 | 40 | ||
| @@ -94,13 +98,13 @@ func (d *KeyboardDispatcher) Dispatch(ctx context.Context, action input.Action) | |||
| 94 | 98 | case input.ActionHardReload: | |
| 95 | 99 | return d.navCoord.HardReload(ctx) | |
| 96 | 100 | ||
| 97 | - | // Zoom (stub - will be implemented later) | |
| 101 | + | // Zoom actions | |
| 98 | 102 | case input.ActionZoomIn: | |
| 99 | - | log.Debug().Msg("zoom in action (not yet implemented)") | |
| 103 | + | return d.handleZoom(ctx, "in") | |
| 100 | 104 | case input.ActionZoomOut: | |
| 101 | - | log.Debug().Msg("zoom out action (not yet implemented)") | |
| 105 | + | return d.handleZoom(ctx, "out") | |
| 102 | 106 | case input.ActionZoomReset: | |
| 103 | - | log.Debug().Msg("zoom reset action (not yet implemented)") | |
| 107 | + | return d.handleZoom(ctx, "reset") | |
| 104 | 108 | ||
| 105 | 109 | // UI | |
| 106 | 110 | case input.ActionOpenOmnibox: | |
| @@ -122,3 +126,63 @@ func (d *KeyboardDispatcher) Dispatch(ctx context.Context, action input.Action) | |||
| 122 | 126 | ||
| 123 | 127 | return nil | |
| 124 | 128 | } | |
| 129 | + | ||
| 130 | + | // handleZoom processes zoom in/out/reset actions for the active WebView. | |
| 131 | + | func (d *KeyboardDispatcher) handleZoom(ctx context.Context, action string) error { | |
| 132 | + | log := logging.FromContext(ctx) | |
| 133 | + | ||
| 134 | + | if d.zoomUC == nil { | |
| 135 | + | log.Warn().Msg("zoom use case not available") | |
| 136 | + | return nil | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | wv := d.navCoord.ActiveWebView(ctx) | |
| 140 | + | if wv == nil { | |
| 141 | + | log.Debug().Msg("no active webview for zoom") | |
| 142 | + | return nil | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | domain, err := usecase.ExtractDomain(wv.URI()) | |
| 146 | + | if err != nil { | |
| 147 | + | log.Debug().Str("uri", wv.URI()).Msg("cannot extract domain for zoom") | |
| 148 | + | return nil | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | current := wv.GetZoomLevel() | |
| 152 | + | var newZoom *entity.ZoomLevel | |
| 153 | + | ||
| 154 | + | switch action { | |
| 155 | + | case "in": | |
| 156 | + | newZoom, err = d.zoomUC.ZoomIn(ctx, domain, current) | |
| 157 | + | case "out": | |
| 158 | + | newZoom, err = d.zoomUC.ZoomOut(ctx, domain, current) | |
| 159 | + | case "reset": | |
| 160 | + | err = d.zoomUC.ResetZoom(ctx, domain) | |
| 161 | + | if err == nil { | |
| 162 | + | newZoom = entity.NewZoomLevel(domain, d.zoomUC.DefaultZoom()) | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | if err != nil { | |
| 167 | + | log.Error().Err(err).Str("action", action).Msg("zoom action failed") | |
| 168 | + | return err | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | if newZoom != nil { | |
| 172 | + | if setErr := wv.SetZoomLevel(ctx, newZoom.ZoomFactor); setErr != nil { | |
| 173 | + | log.Error().Err(setErr).Float64("zoom", newZoom.ZoomFactor).Msg("failed to apply zoom") | |
| 174 | + | return setErr | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | // Notify omnibox to update zoom indicator | |
| 178 | + | d.navCoord.NotifyZoomChanged(ctx, newZoom.ZoomFactor) | |
| 179 | + | ||
| 180 | + | log.Debug(). | |
| 181 | + | Str("domain", domain). | |
| 182 | + | Str("action", action). | |
| 183 | + | Float64("zoom", newZoom.ZoomFactor). | |
| 184 | + | Msg("zoom applied") | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | return nil | |
| 188 | + | } | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | 5 | ||||
| 6 | "github.com/bnema/dumber/internal/application/usecase" | 6 | "github.com/bnema/dumber/internal/application/usecase" | ||
| 7 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 7 | "github.com/bnema/dumber/internal/logging" | 8 | "github.com/bnema/dumber/internal/logging" | ||
| 8 | "github.com/bnema/dumber/internal/ui/coordinator" | 9 | "github.com/bnema/dumber/internal/ui/coordinator" | ||
| 9 | "github.com/bnema/dumber/internal/ui/input" | 10 | "github.com/bnema/dumber/internal/ui/input" | ||
| @@ -14,6 +15,7 @@ type KeyboardDispatcher struct { | |||||
| 14 | tabCoord *coordinator.TabCoordinator | 15 | tabCoord *coordinator.TabCoordinator | ||
| 15 | wsCoord *coordinator.WorkspaceCoordinator | 16 | wsCoord *coordinator.WorkspaceCoordinator | ||
| 16 | navCoord *coordinator.NavigationCoordinator | 17 | navCoord *coordinator.NavigationCoordinator | ||
| 18 | + | zoomUC *usecase.ManageZoomUseCase | |||
| 17 | onQuit func() | 19 | onQuit func() | ||
| 18 | } | 20 | } | ||
| 19 | 21 | ||||
| @@ -23,6 +25,7 @@ func NewKeyboardDispatcher( | |||||
| 23 | tabCoord *coordinator.TabCoordinator, | 25 | tabCoord *coordinator.TabCoordinator, | ||
| 24 | wsCoord *coordinator.WorkspaceCoordinator, | 26 | wsCoord *coordinator.WorkspaceCoordinator, | ||
| 25 | navCoord *coordinator.NavigationCoordinator, | 27 | navCoord *coordinator.NavigationCoordinator, | ||
| 28 | + | zoomUC *usecase.ManageZoomUseCase, | |||
| 26 | ) *KeyboardDispatcher { | 29 | ) *KeyboardDispatcher { | ||
| 27 | log := logging.FromContext(ctx) | 30 | log := logging.FromContext(ctx) | ||
| 28 | log.Debug().Msg("creating keyboard dispatcher") | 31 | log.Debug().Msg("creating keyboard dispatcher") | ||
| @@ -31,6 +34,7 @@ func NewKeyboardDispatcher( | |||||
| 31 | tabCoord: tabCoord, | 34 | tabCoord: tabCoord, | ||
| 32 | wsCoord: wsCoord, | 35 | wsCoord: wsCoord, | ||
| 33 | navCoord: navCoord, | 36 | navCoord: navCoord, | ||
| 37 | + | zoomUC: zoomUC, | |||
| 34 | } | 38 | } | ||
| 35 | } | 39 | } | ||
| 36 | 40 | ||||
| @@ -94,13 +98,13 @@ func (d *KeyboardDispatcher) Dispatch(ctx context.Context, action input.Action) | |||||
| 94 | case input.ActionHardReload: | 98 | case input.ActionHardReload: | ||
| 95 | return d.navCoord.HardReload(ctx) | 99 | return d.navCoord.HardReload(ctx) | ||
| 96 | 100 | ||||
| 97 | - | // Zoom (stub - will be implemented later) | 101 | + | // Zoom actions |
| 98 | case input.ActionZoomIn: | 102 | case input.ActionZoomIn: | ||
| 99 | - | log.Debug().Msg("zoom in action (not yet implemented)") | 103 | + | return d.handleZoom(ctx, "in") |
| 100 | case input.ActionZoomOut: | 104 | case input.ActionZoomOut: | ||
| 101 | - | log.Debug().Msg("zoom out action (not yet implemented)") | 105 | + | return d.handleZoom(ctx, "out") |
| 102 | case input.ActionZoomReset: | 106 | case input.ActionZoomReset: | ||
| 103 | - | log.Debug().Msg("zoom reset action (not yet implemented)") | 107 | + | return d.handleZoom(ctx, "reset") |
| 104 | 108 | ||||
| 105 | // UI | 109 | // UI | ||
| 106 | case input.ActionOpenOmnibox: | 110 | case input.ActionOpenOmnibox: | ||
| @@ -122,3 +126,63 @@ func (d *KeyboardDispatcher) Dispatch(ctx context.Context, action input.Action) | |||||
| 122 | 126 | ||||
| 123 | return nil | 127 | return nil | ||
| 124 | } | 128 | } | ||
| 129 | + | ||||
| 130 | + | // handleZoom processes zoom in/out/reset actions for the active WebView. | |||
| 131 | + | func (d *KeyboardDispatcher) handleZoom(ctx context.Context, action string) error { | |||
| 132 | + | log := logging.FromContext(ctx) | |||
| 133 | + | ||||
| 134 | + | if d.zoomUC == nil { | |||
| 135 | + | log.Warn().Msg("zoom use case not available") | |||
| 136 | + | return nil | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | wv := d.navCoord.ActiveWebView(ctx) | |||
| 140 | + | if wv == nil { | |||
| 141 | + | log.Debug().Msg("no active webview for zoom") | |||
| 142 | + | return nil | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | domain, err := usecase.ExtractDomain(wv.URI()) | |||
| 146 | + | if err != nil { | |||
| 147 | + | log.Debug().Str("uri", wv.URI()).Msg("cannot extract domain for zoom") | |||
| 148 | + | return nil | |||
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | current := wv.GetZoomLevel() | |||
| 152 | + | var newZoom *entity.ZoomLevel | |||
| 153 | + | ||||
| 154 | + | switch action { | |||
| 155 | + | case "in": | |||
| 156 | + | newZoom, err = d.zoomUC.ZoomIn(ctx, domain, current) | |||
| 157 | + | case "out": | |||
| 158 | + | newZoom, err = d.zoomUC.ZoomOut(ctx, domain, current) | |||
| 159 | + | case "reset": | |||
| 160 | + | err = d.zoomUC.ResetZoom(ctx, domain) | |||
| 161 | + | if err == nil { | |||
| 162 | + | newZoom = entity.NewZoomLevel(domain, d.zoomUC.DefaultZoom()) | |||
| 163 | + | } | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | if err != nil { | |||
| 167 | + | log.Error().Err(err).Str("action", action).Msg("zoom action failed") | |||
| 168 | + | return err | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | if newZoom != nil { | |||
| 172 | + | if setErr := wv.SetZoomLevel(ctx, newZoom.ZoomFactor); setErr != nil { | |||
| 173 | + | log.Error().Err(setErr).Float64("zoom", newZoom.ZoomFactor).Msg("failed to apply zoom") | |||
| 174 | + | return setErr | |||
| 175 | + | } | |||
| 176 | + | ||||
| 177 | + | // Notify omnibox to update zoom indicator | |||
| 178 | + | d.navCoord.NotifyZoomChanged(ctx, newZoom.ZoomFactor) | |||
| 179 | + | ||||
| 180 | + | log.Debug(). | |||
| 181 | + | Str("domain", domain). | |||
| 182 | + | Str("action", action). | |||
| 183 | + | Float64("zoom", newZoom.ZoomFactor). | |||
| 184 | + | Msg("zoom applied") | |||
| 185 | + | } | |||
| 186 | + | ||||
| 187 | + | return nil | |||
| 188 | + | } | |||