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,
),
);