diff --git a/CHANGES_CURRENT.md b/CHANGES_CURRENT.md index 347820b2e2..a46176b768 100644 --- a/CHANGES_CURRENT.md +++ b/CHANGES_CURRENT.md @@ -8,6 +8,10 @@ - #3718 - Completion: Add `editor.suggest.itemsToShow` setting (fixes #3712) - #3736 - Search: add default keys to go to next / previous search result (fixes #3713) - #3733 - Quick Open: Add bindings to open in splits, not current buffer. +- #3765 - UX: Add `"window.titleBarStyle"` configuration setting + +> __BREAKING:__ On Windows, the default setting is to use the `"native"` title bar. +> Set `"window.titleBarStyle": "custom"` to keep the previous behavior. ### Bug Fixes diff --git a/bench/lib/Helpers.re b/bench/lib/Helpers.re index 68b46ffbbc..5a3a8c5634 100644 --- a/bench/lib/Helpers.re +++ b/bench/lib/Helpers.re @@ -39,6 +39,7 @@ let simpleState = { ~titlebarHeight=0., ~getZoom=() => 1.0, ~setZoom=_zoom => (), + ~useNativeTitleBar=true, ); Reducer.reduce( diff --git a/docs/docs/configuration/settings.md b/docs/docs/configuration/settings.md index 39e7dcaa6f..429788a50b 100644 --- a/docs/docs/configuration/settings.md +++ b/docs/docs/configuration/settings.md @@ -181,6 +181,8 @@ The configuration file, `configuration.json` is in the Oni2 directory, whose loc - `window.menuBarVisibility` __(_"visible" | "hidden"_ default: `"visible"`)__ - Controls the visibility of the menu bar. +- `window.titleBarStyle` __(_"native" | "custom"_ default: `"native"` on Windows, `"custom"` otherwise)__ - Controls whether the titlebar is custom-rendered. + - `oni.layout.showLayoutTabs` __(_"always"|"smart"|"never"_ default: `"smart"`)__ - Controls the display of layout tabs. `"smart"` will only show the tabs if there's more than one. - `oni.layout.layoutTabPosition` __(_"top"|"bottom"_ default: `"bottom"`)__ - Controls the position of the layout tabs. diff --git a/integration_test/lib/Oni_IntegrationTestLib.re b/integration_test/lib/Oni_IntegrationTestLib.re index 649015d4e7..5ed3fb19d6 100644 --- a/integration_test/lib/Oni_IntegrationTestLib.re +++ b/integration_test/lib/Oni_IntegrationTestLib.re @@ -190,6 +190,7 @@ let runTest = ~titlebarHeight=0., ~setZoom, ~getZoom, + ~useNativeTitleBar=true, ), ); diff --git a/src/Feature/Configuration/Feature_Configuration.rei b/src/Feature/Configuration/Feature_Configuration.rei index 772918f92a..9b67e1f914 100644 --- a/src/Feature/Configuration/Feature_Configuration.rei +++ b/src/Feature/Configuration/Feature_Configuration.rei @@ -11,6 +11,8 @@ module ConfigurationLoader: { let none: t; let file: FpExp.t(FpExp.absolute) => t; + + let loadImmediate: t => result(Config.Settings.t, string); }; let initial: diff --git a/src/Feature/Configuration/GlobalConfiguration.re b/src/Feature/Configuration/GlobalConfiguration.re index 7b6355e577..5b1a978462 100644 --- a/src/Feature/Configuration/GlobalConfiguration.re +++ b/src/Feature/Configuration/GlobalConfiguration.re @@ -46,6 +46,16 @@ module Decoders = { ), ), ]); + + let titleBarStyle: decoder([ | `Native | `Custom]) = + string + |> map(String.lowercase_ascii) + |> map( + fun + | "native" => `Native + | "custom" => `Custom + | _ => `Custom, + ); }; module Encoders = { @@ -57,6 +67,11 @@ module Encoders = { | `None => string("none") | `Inline => string("inline") }; + + let titleBarStyle: encoder([ | `Native | `Custom]) = + fun + | `Native => string("native") + | `Custom => string("custom"); }; module Codecs = { @@ -292,6 +307,24 @@ module Search = { let followSymlinks = setting("search.followSymlinks", bool, ~default=true); }; +module Window = { + // On Windows, default the titlebar style to native, due to various bugs around the custom-rendered window, like: + // #3730 - Keyboard shortcuts for window movement broken + // #3071 - Window has no shadow on Windows + // #3063 - Part of onivim fullscreen is visible on second monitor + let defaultTitleBarStyle = + switch (Revery.Environment.os) { + | Windows(_) => `Native + | _ => `Custom + }; + let titleBarStyle = + setting( + "window.titleBarStyle", + custom(~decode=Decoders.titleBarStyle, ~encode=Encoders.titleBarStyle), + ~default=defaultTitleBarStyle, + ); +}; + module Workbench = { let activityBarVisible = setting("workbench.activityBar.visible", bool, ~default=true); @@ -319,6 +352,7 @@ let contributions = [ Editor.snippetSuggestions.spec, Files.exclude.spec, Explorer.autoReveal.spec, + Window.titleBarStyle.spec, Workbench.activityBarVisible.spec, Workbench.editorShowTabs.spec, Workbench.editorEnablePreview.spec, diff --git a/src/Feature/TitleBar/Feature_TitleBar.re b/src/Feature/TitleBar/Feature_TitleBar.re index 051a1157d1..096768ec43 100644 --- a/src/Feature/TitleBar/Feature_TitleBar.re +++ b/src/Feature/TitleBar/Feature_TitleBar.re @@ -15,6 +15,17 @@ type msg = | WindowCloseClicked | TitleDoubleClicked; +type model = { + // We need to store this, as opposed to pulling it + // from config, because we need to respect the value + // used on initialization. + useNativeTitleBar: bool, +}; + +let isNative = ({useNativeTitleBar, _}) => useNativeTitleBar; + +let initial = (~useNativeTitleBar) => {useNativeTitleBar: useNativeTitleBar}; + module Log = (val Log.withNamespace("Oni2.Feature.TitleBar")); module Internal = { @@ -155,7 +166,7 @@ type outmsg = | Nothing | Effect(Isolinear.Effect.t(msg)); -let update = (~maximize, ~minimize, ~restore, ~close, msg) => { +let update = (~maximize, ~minimize, ~restore, ~close, msg, model) => { let internalDoubleClickEffect = Isolinear.Effect.create(~name="window.doubleClick", () => { switch (Internal.getTitleDoubleClickBehavior()) { @@ -174,11 +185,11 @@ let update = (~maximize, ~minimize, ~restore, ~close, msg) => { Isolinear.Effect.create(~name="window.restore", () => restore()); switch (msg) { - | TitleDoubleClicked => Effect(internalDoubleClickEffect) - | WindowCloseClicked => Effect(internalWindowCloseEffect) - | WindowMaximizeClicked => Effect(internalWindowMaximizeEffect) - | WindowRestoreClicked => Effect(internalWindowRestoreEffect) - | WindowMinimizeClicked => Effect(internalWindowMinimizeEffect) + | TitleDoubleClicked => (model, Effect(internalDoubleClickEffect)) + | WindowCloseClicked => (model, Effect(internalWindowCloseEffect)) + | WindowMaximizeClicked => (model, Effect(internalWindowMaximizeEffect)) + | WindowRestoreClicked => (model, Effect(internalWindowRestoreEffect)) + | WindowMinimizeClicked => (model, Effect(internalWindowMinimizeEffect)) }; }; @@ -537,11 +548,13 @@ module View = { ~theme, ~font: UiFont.t, ~height, + ~model, (), ) => { let title = title(~activeBuffer, ~workspaceRoot, ~workspaceDirectory, ~config); switch (Revery.Environment.os) { + | Mac(_) when isNative(model) => React.empty | Mac({major, _}) => + | Windows(_) when isNative(model) => menuBar | Windows(_) => model; + +let isNative: model => bool; + [@deriving show] type msg; @@ -32,9 +38,10 @@ let update: ~minimize: unit => unit, ~restore: unit => unit, ~close: unit => unit, - msg + msg, + model ) => - outmsg; + (model, outmsg); // VIEW @@ -54,6 +61,7 @@ module View: { ~theme: ColorTheme.Colors.t, ~font: UiFont.t, ~height: float, + ~model: model, unit ) => Revery.UI.element; diff --git a/src/Model/State.re b/src/Model/State.re index dd5815aba7..3d3769276c 100644 --- a/src/Model/State.re +++ b/src/Model/State.re @@ -493,6 +493,7 @@ type t = { modal: option(Feature_Modals.model), snippets: Feature_Snippets.model, textContentProviders: list((int, string)), + titleBar: Feature_TitleBar.model, vim: Feature_Vim.model, zoom: Feature_Zoom.model, autoUpdate: Feature_AutoUpdate.model, @@ -516,6 +517,7 @@ let initial = ~titlebarHeight, ~getZoom, ~setZoom, + ~useNativeTitleBar, ) => { let config = Feature_Configuration.initial( @@ -668,6 +670,7 @@ let initial = quickOpen: Feature_QuickOpen.initial, snippets: Feature_Snippets.initial, terminals: Feature_Terminal.initial, + titleBar: Feature_TitleBar.initial(~useNativeTitleBar), textContentProviders: [], vim: Feature_Vim.initial, zoom: Feature_Zoom.initial(~getZoom, ~setZoom), diff --git a/src/Store/Features.re b/src/Store/Features.re index 9f0c5442a4..53f1309106 100644 --- a/src/Store/Features.re +++ b/src/Store/Features.re @@ -2542,21 +2542,25 @@ let update = ); | TitleBar(titleBarMsg) => + let (titleBar', outmsg) = + Feature_TitleBar.update( + ~maximize, + ~minimize, + ~close, + ~restore, + titleBarMsg, + state.titleBar, + ); let eff = - switch ( - Feature_TitleBar.update( - ~maximize, - ~minimize, - ~close, - ~restore, - titleBarMsg, - ) - ) { + switch (outmsg) { | Feature_TitleBar.Effect(effect) => effect | Feature_TitleBar.Nothing => Isolinear.Effect.none }; - (state, eff |> Isolinear.Effect.map(msg => TitleBar(msg))); + ( + {...state, titleBar: titleBar'}, + eff |> Isolinear.Effect.map(msg => TitleBar(msg)), + ); | ExtensionBufferUpdateQueued({triggerKey}) => let maybeBuffer = Selectors.getActiveBuffer(state); diff --git a/src/UI/Root.re b/src/UI/Root.re index 7ecb3682ec..69b2030f98 100644 --- a/src/UI/Root.re +++ b/src/UI/Root.re @@ -20,7 +20,7 @@ module Constants = { module Styles = { open Style; - let root = (theme, windowDisplayMode) => { + let root = (~nativeTitleBar, theme, windowDisplayMode) => { let style = ref([ backgroundColor(Colors.Editor.background.from(theme)), @@ -33,7 +33,9 @@ module Styles = { justifyContent(`Center), alignItems(`Stretch), ]); - if (Revery.Environment.isWindows && windowDisplayMode == State.Maximized) { + if (Revery.Environment.isWindows + && windowDisplayMode == State.Maximized + && !nativeTitleBar) { style := [margin(6), ...style^]; }; style^; @@ -252,7 +254,9 @@ let make = (~dispatch, ~state: State.t, ()) => { // Correct for zoom in title bar height let titlebarHeight = state.titlebarHeight /. zoom; - + let nativeTitleBar = Feature_TitleBar.isNative(state.titleBar); + + { dispatch=titleDispatch registrationDispatch height=titlebarHeight + model={state.titleBar} /> diff --git a/src/bin_editor/Oni2_editor.re b/src/bin_editor/Oni2_editor.re index dc561a64d7..71a580dc5d 100644 --- a/src/bin_editor/Oni2_editor.re +++ b/src/bin_editor/Oni2_editor.re @@ -162,7 +162,8 @@ switch (eff) { |> Option.map(pos => `Absolute(pos)) |> Option.value(~default=`Centered); - let createWindow = (~forceScaleFactor, ~maybeWorkspace, app) => { + let createWindow = + (~configurationLoader, ~forceScaleFactor, ~maybeWorkspace, app) => { let (x, y, width, height, maximized) = { Store.Persistence.Workspace.( maybeWorkspace @@ -204,12 +205,39 @@ switch (eff) { ) ); + let settings = + Feature_Configuration.ConfigurationLoader.loadImmediate( + configurationLoader, + ) + |> Result.value(~default=Oni_Core.Config.Settings.empty); + + let initialResolver = (~vimSetting as _, settingKey) => { + Oni_Core.Config.( + { + Settings.get(settingKey, settings) + |> Option.map(json => Json(json)) + |> Option.value(~default=NotSet); + } + ); + }; + + let useNativeMenu = + Feature_Configuration.GlobalConfiguration.Window.titleBarStyle.get( + initialResolver, + ) + == `Native; + let decorated = switch (Revery.Environment.os) { - | Windows(_) => false + | Windows(_) when useNativeMenu => true + | Windows(_) when !useNativeMenu => false | _ => true }; + let titlebarStyle = + useNativeMenu + ? Revery.WindowStyles.System : Revery.WindowStyles.Transparent; + let icon = switch (Revery.Environment.os) { | Mac(_) => @@ -229,7 +257,7 @@ switch (eff) { ~maximized, ~vsync=Vsync.Immediate, ~icon, - ~titlebarStyle=WindowStyles.Transparent, + ~titlebarStyle, ~x, ~y, ~width, @@ -244,7 +272,7 @@ switch (eff) { Window.setBackgroundColor(window, Colors.black); win := Some(window); - window; + (window, useNativeMenu); }; Log.infof(m => m( @@ -281,8 +309,25 @@ switch (eff) { |> Option.map(FpExp.toString) |> Option.value(~default=initialWorkingDirectory); - let window = + let configurationLoader = + Feature_Configuration.( + if (!cliOptions.shouldLoadConfiguration) { + ConfigurationLoader.none; + } else { + Oni_Core.Filesystem.getOrCreateConfigFile("configuration.json") + |> Result.map(ConfigurationLoader.file) + |> Oni_Core.Utility.ResultEx.tapError(msg => + Log.errorf(m => + m("Error initializing configuration file: %s", msg) + ) + ) + |> Result.value(~default=ConfigurationLoader.none); + } + ); + + let (window, useNativeMenu) = createWindow( + ~configurationLoader, ~forceScaleFactor=cliOptions.forceScaleFactor, ~maybeWorkspace, app, @@ -339,22 +384,6 @@ switch (eff) { ) |> Result.value(~default=Feature_Input.KeybindingsLoader.none); - let configurationLoader = - Feature_Configuration.( - if (!cliOptions.shouldLoadConfiguration) { - ConfigurationLoader.none; - } else { - Oni_Core.Filesystem.getOrCreateConfigFile("configuration.json") - |> Result.map(ConfigurationLoader.file) - |> Oni_Core.Utility.ResultEx.tapError(msg => - Log.errorf(m => - m("Error initializing configurationj file: %s", msg) - ) - ) - |> Result.value(~default=ConfigurationLoader.none); - } - ); - let currentState = ref( Model.State.initial( @@ -373,6 +402,7 @@ switch (eff) { ~titlebarHeight=Revery.Window.getTitlebarHeight(window), ~setZoom, ~getZoom, + ~useNativeTitleBar=useNativeMenu, ), );