-
+
+ <%= unless @is_mac_ui do %>
+
+ <% end %>
<%= @page_title %>
@@ -47,6 +64,36 @@
+ <%= if not @is_mac_ui do %>
+ <%= if group = active_titlebar_menu_group(assigns) do %>
+
+ <% end %>
+ <% end %>
diff --git a/priv/ui/app.css b/priv/ui/app.css
index a5d05a0..29eb90b 100644
--- a/priv/ui/app.css
+++ b/priv/ui/app.css
@@ -146,6 +146,10 @@ button {
background-color: var(--vscode-toolbar-hoverBackground);
}
+.window-titlebar-menu-button.is-active {
+ background-color: var(--vscode-toolbar-hoverBackground);
+}
+
.window-titlebar-menu-button:focus,
.window-titlebar-menu-button:focus-visible,
.window-titlebar-action-button:focus,
@@ -154,6 +158,61 @@ button {
box-shadow: none;
}
+.window-titlebar-menu-dropdown {
+ position: absolute;
+ top: 30px;
+ left: 6px;
+ min-width: 210px;
+ padding: 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ background-color: var(--vscode-menu-background, var(--vscode-editorWidget-background));
+ border: 1px solid var(--vscode-menu-border, var(--vscode-panel-border));
+ border-radius: 6px;
+ box-shadow: var(--vscode-widget-shadow, 0 8px 24px rgba(0, 0, 0, 0.4));
+ app-region: no-drag;
+ -webkit-app-region: no-drag;
+ z-index: 10;
+}
+
+.window-titlebar-menu-item {
+ border: none;
+ background: transparent;
+ color: var(--vscode-menu-foreground, var(--vscode-foreground));
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ text-align: left;
+ border-radius: 4px;
+ padding: 6px 8px;
+ font-size: 12px;
+ cursor: pointer;
+}
+
+.window-titlebar-menu-item:focus,
+.window-titlebar-menu-item:focus-visible {
+ outline: none;
+ box-shadow: none;
+ background-color: var(--vscode-toolbar-hoverBackground);
+}
+
+.window-titlebar-menu-item:hover,
+.window-titlebar-menu-item.is-keyboard-active {
+ background-color: var(--vscode-menu-selectionBackground, var(--vscode-toolbar-hoverBackground));
+}
+
+.window-titlebar-menu-item-accelerator {
+ opacity: 0.8;
+}
+
+.window-titlebar-menu-separator {
+ height: 1px;
+ margin: 4px 2px;
+ background-color: var(--vscode-menu-separatorBackground, rgba(255, 255, 255, 0.08));
+}
+
.window-titlebar-drag-region {
flex: 1;
height: 100%;
diff --git a/priv/ui/live.js b/priv/ui/live.js
index 43c90f3..79593a2 100644
--- a/priv/ui/live.js
+++ b/priv/ui/live.js
@@ -183,6 +183,27 @@ document.addEventListener("DOMContentLoaded", () => {
}
};
+ this.menuIsOpen = () => {
+ const titlebar = this.el.querySelector("[data-testid='window-titlebar']");
+ return Boolean(titlebar?.dataset.openMenuGroup);
+ };
+
+ this.handleTitlebarPointerDown = (event) => {
+ if (!this.menuIsOpen()) {
+ return;
+ }
+
+ if (event.target.closest("[data-testid='window-titlebar-menu-button']")) {
+ return;
+ }
+
+ if (event.target.closest("[data-testid='window-titlebar-menu-dropdown']")) {
+ return;
+ }
+
+ this.pushEvent("close_titlebar_menu", {});
+ };
+
this.handleChange = (event) => {
const select = event.target.closest(".status-bar-language-select");
@@ -216,8 +237,16 @@ document.addEventListener("DOMContentLoaded", () => {
});
};
+ this.handleEscapeKey = (event) => {
+ if (event.key === "Escape" && this.menuIsOpen()) {
+ this.pushEvent("close_titlebar_menu", {});
+ }
+ };
+
window.addEventListener("bds:native-menu-action", this.handleNativeMenuAction);
window.addEventListener("keydown", this.handleShortcutKeyDown, true);
+ window.addEventListener("keydown", this.handleEscapeKey, true);
+ window.addEventListener("pointerdown", this.handleTitlebarPointerDown, true);
this.el.addEventListener("change", this.handleChange);
},
@@ -226,6 +255,8 @@ document.addEventListener("DOMContentLoaded", () => {
this.el.removeEventListener("change", this.handleChange);
window.removeEventListener("bds:native-menu-action", this.handleNativeMenuAction);
window.removeEventListener("keydown", this.handleShortcutKeyDown, true);
+ window.removeEventListener("keydown", this.handleEscapeKey, true);
+ window.removeEventListener("pointerdown", this.handleTitlebarPointerDown, true);
if (this.destroyOverlaySync) {
this.destroyOverlaySync();
}
diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs
index fc1f670..c6b2e56 100644
--- a/test/bds/desktop/shell_live_test.exs
+++ b/test/bds/desktop/shell_live_test.exs
@@ -25,6 +25,16 @@ defmodule BDS.Desktop.ShellLiveTest do
{:ok, project} = Projects.create_project(%{name: "Shell Project", data_path: temp_dir})
{:ok, _project} = Projects.set_active_project(project.id)
+ original_shell_platform = Application.get_env(:bds, :shell_platform)
+
+ on_exit(fn ->
+ if is_nil(original_shell_platform) do
+ Application.delete_env(:bds, :shell_platform)
+ else
+ Application.put_env(:bds, :shell_platform, original_shell_platform)
+ end
+ end)
+
%{project: project, temp_dir: temp_dir}
end
@@ -95,6 +105,59 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(class="tab-bar-empty")
end
+ test "titlebar menu stays hidden on macos because the native menu owns it" do
+ {:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
+
+ assert html =~ ~s(class="window-titlebar is-mac")
+ refute html =~ ~s(data-testid="window-titlebar-menu-bar")
+ refute html =~ ~s(data-testid="window-titlebar-menu-button")
+ refute html =~ ~s(data-testid="window-titlebar-menu-dropdown")
+
+ html =
+ render_hook(view, "native_menu_action", %{"action" => "edit_preferences"})
+
+ assert html =~ ~s(data-tab-type="settings")
+ assert html =~ ">Settings<"
+ end
+
+ test "titlebar menu matches the old shell contract on windows and linux" do
+ Application.put_env(:bds, :shell_platform, {:unix, :linux})
+
+ {:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
+
+ refute html =~ ~s(class="window-titlebar is-mac")
+ assert html =~ ~s(data-testid="window-titlebar-menu-bar")
+ assert html =~ ~s(data-testid="window-titlebar-menu-button")
+ assert html =~ ~s(data-menu-group="file")
+ assert html =~ ~s(>File<)
+
+ html =
+ view
+ |> element("[data-testid='window-titlebar-menu-button'][data-menu-group='file']")
+ |> render_click()
+
+ assert html =~ ~s(data-testid="window-titlebar-menu-dropdown")
+ assert html =~ ~s(data-testid="window-titlebar-menu-item")
+ assert html =~ ~s(data-menu-action="new_post")
+ assert html =~ ~s(>New Post<)
+
+ html =
+ view
+ |> element("[data-testid='window-titlebar-menu-button'][data-menu-group='edit']")
+ |> render_click()
+
+ assert html =~ ~s(data-menu-action="edit_preferences")
+
+ html =
+ view
+ |> element("[data-testid='window-titlebar-menu-item'][data-menu-action='edit_preferences']")
+ |> render_click()
+
+ assert html =~ ~s(data-tab-type="settings")
+ assert html =~ ">Settings<"
+ refute html =~ ~s(data-testid="window-titlebar-menu-dropdown")
+ end
+
test "sidebar open supports preview and pin intents for entity tabs" do
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
diff --git a/test/bds/desktop_test.exs b/test/bds/desktop_test.exs
index d504c48..6209455 100644
--- a/test/bds/desktop_test.exs
+++ b/test/bds/desktop_test.exs
@@ -101,7 +101,9 @@ defmodule BDS.DesktopTest do
assert conn.status == 200
assert conn.resp_body =~ ~s(class="app")
- assert conn.resp_body =~ ~s(class="window-titlebar")
+ assert conn.resp_body =~ ~s(data-testid="window-titlebar")
+ assert conn.resp_body =~ ~s(class="window-titlebar is-mac")
+ refute conn.resp_body =~ ~s(data-testid="window-titlebar-menu-bar")
assert conn.resp_body =~ ~s(class="activity-bar")
assert conn.resp_body =~ ~s(class="sidebar")
assert conn.resp_body =~ ~s(class="status-bar")