diff --git a/assets/translations/en.json b/assets/translations/en.json index 6c6af8b02b..e557c8fa16 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -1824,6 +1824,10 @@ "description": "Briefly reveal the bar when the active workspace changes", "label": "Show on Workspace Switch" }, + "auto-hide-reserve-space": { + "description": "With auto-hide or smart auto-hide, reserve layout only while the bar is visible so hidden windows use the full screen", + "label": "Reserve Space While Visible" + }, "start-widgets": { "description": "Widgets on the start (left/top) side of the bar", "label": "Start Widgets" diff --git a/example.toml b/example.toml index 0c26d14fe2..e606b08445 100644 --- a/example.toml +++ b/example.toml @@ -348,6 +348,7 @@ font_scale = 1.0 # text-only scale multiplier across widgets shadow = true auto_hide = false # smart_auto_hide = false # show when the active workspace is empty; hide when it has windows +# auto_hide_reserve_space = false # with auto_hide/smart_auto_hide: reserve layout only while the bar is visible # show_on_workspace_switch = true # with auto_hide: briefly reveal when the active workspace changes reserve_space = true capsule = false diff --git a/meson.build b/meson.build index 05119f2090..e630a10cb3 100644 --- a/meson.build +++ b/meson.build @@ -1129,6 +1129,7 @@ if build_tests 'app_identity', 'audio_glyphs', 'audio_route_selection', + 'bar_exclusive_zone', 'battery_health', 'battery_hook_state', 'button_layout', diff --git a/src/config/config_export.cpp b/src/config/config_export.cpp index 3a7b788889..7a5300d856 100644 --- a/src/config/config_export.cpp +++ b/src/config/config_export.cpp @@ -141,6 +141,8 @@ namespace config_export { resolved.autoHide = *ovr.autoHide; if (ovr.smartAutoHide) resolved.smartAutoHide = *ovr.smartAutoHide; + if (ovr.autoHideReserveSpace) + resolved.autoHideReserveSpace = *ovr.autoHideReserveSpace; if (ovr.showOnWorkspaceSwitch) resolved.showOnWorkspaceSwitch = *ovr.showOnWorkspaceSwitch; if (ovr.reserveSpace) diff --git a/src/config/config_overrides.cpp b/src/config/config_overrides.cpp index 3c6af157c3..b305253432 100644 --- a/src/config/config_overrides.cpp +++ b/src/config/config_overrides.cpp @@ -175,6 +175,9 @@ namespace { if (ovr.smartAutoHide) { resolved.smartAutoHide = *ovr.smartAutoHide; } + if (ovr.autoHideReserveSpace) { + resolved.autoHideReserveSpace = *ovr.autoHideReserveSpace; + } if (ovr.showOnWorkspaceSwitch) { resolved.showOnWorkspaceSwitch = *ovr.showOnWorkspaceSwitch; } diff --git a/src/config/config_service.cpp b/src/config/config_service.cpp index 2d5cc7a6b0..775363e815 100644 --- a/src/config/config_service.cpp +++ b/src/config/config_service.cpp @@ -937,6 +937,8 @@ BarConfig ConfigService::resolveForOutput(const BarConfig& base, const WaylandOu resolved.autoHide = *ovr.autoHide; if (ovr.smartAutoHide) resolved.smartAutoHide = *ovr.smartAutoHide; + if (ovr.autoHideReserveSpace) + resolved.autoHideReserveSpace = *ovr.autoHideReserveSpace; if (ovr.showOnWorkspaceSwitch) resolved.showOnWorkspaceSwitch = *ovr.showOnWorkspaceSwitch; if (ovr.reserveSpace) diff --git a/src/config/config_types.h b/src/config/config_types.h index 0a5d685f55..aa82ed0040 100644 --- a/src/config/config_types.h +++ b/src/config/config_types.h @@ -67,6 +67,7 @@ struct BarMonitorOverride { std::optional enabled; std::optional autoHide; std::optional smartAutoHide; + std::optional autoHideReserveSpace; std::optional showOnWorkspaceSwitch; std::optional reserveSpace; std::optional layer; // top | overlay @@ -133,10 +134,11 @@ struct BarConfig { bool enabled = true; bool autoHide = false; // slide out when the pointer leaves; reveal on edge approach bool smartAutoHide = false; // hide while the active workspace has windows; show when it is empty + bool autoHideReserveSpace = false; // with auto_hide/smart_auto_hide: reserve layout only while the bar is visible bool showOnWorkspaceSwitch = true; // with auto_hide: briefly reveal when the active workspace changes [[nodiscard]] constexpr bool isAutoHideEnabled() const noexcept { return autoHide || smartAutoHide; } - bool reserveSpace = true; // reserve compositor exclusive zone; applies with or without auto_hide + bool reserveSpace = true; // reserve compositor exclusive zone std::string layer = "top"; // top | overlay — attached panels use the same layer std::int32_t thickness = Style::barThicknessDefault; float backgroundOpacity = 1.0F; diff --git a/src/config/schema/config_schema.cpp b/src/config/schema/config_schema.cpp index b35e981fe0..38f8140b92 100644 --- a/src/config/schema/config_schema.cpp +++ b/src/config/schema/config_schema.cpp @@ -2182,6 +2182,7 @@ namespace noctalia::config::schema { field(&BarConfig::smartAutoHide, "smart_auto_hide"), field(&BarConfig::showOnWorkspaceSwitch, "show_on_workspace_switch"), field(&BarConfig::reserveSpace, "reserve_space"), + field(&BarConfig::autoHideReserveSpace, "auto_hide_reserve_space"), barLayerField(), field(&BarConfig::thickness, "thickness", kBarThicknessRange), field(&BarConfig::backgroundOpacity, "background_opacity", kBarOpacityRange), @@ -2238,6 +2239,7 @@ namespace noctalia::config::schema { optionalBoolField(&BarMonitorOverride::smartAutoHide, "smart_auto_hide"), optionalBoolField(&BarMonitorOverride::showOnWorkspaceSwitch, "show_on_workspace_switch"), optionalBoolField(&BarMonitorOverride::reserveSpace, "reserve_space"), + optionalBoolField(&BarMonitorOverride::autoHideReserveSpace, "auto_hide_reserve_space"), // layer accepts top|overlay; anything else warns and leaves it unset. custom( "layer", diff --git a/src/shell/bar/bar.cpp b/src/shell/bar/bar.cpp index cbdde44d96..3050777a4c 100644 --- a/src/shell/bar/bar.cpp +++ b/src/shell/bar/bar.cpp @@ -1942,10 +1942,9 @@ bool Bar::barContentVisuallyShown(const BarInstance& instance) const noexcept { } bool Bar::shouldReserveExclusiveZone(const BarInstance& instance) const noexcept { - if (instance.ipcLayoutReleased) { - return false; - } - return instance.barConfig.reserveSpace; + return barShouldReserveExclusiveZone( + instance.barConfig, instance.ipcLayoutReleased, barContentVisuallyShown(instance) + ); } void Bar::syncBarExclusiveZone(BarInstance& instance) { diff --git a/src/shell/bar/bar_reserved_zone.h b/src/shell/bar/bar_reserved_zone.h index 781cd2c60d..e4e5b5168b 100644 --- a/src/shell/bar/bar_reserved_zone.h +++ b/src/shell/bar/bar_reserved_zone.h @@ -70,3 +70,16 @@ barEdgeLayerMargin(const BarConfig& barConfig, const ShellConfig::ShadowConfig& reservedBarEdgeDistance(const BarConfig& barConfig, const ShellConfig::ShadowConfig& shadowConfig) { return reservedBarExclusiveZone(barConfig, shadowConfig) + barEdgeLayerMargin(barConfig, shadowConfig); } + +/// Whether the bar should publish a non-zero layer-shell exclusive zone right now. +/// With auto_hide_reserve_space enabled, reserve_space applies only while the bar is shown. +[[nodiscard]] inline bool +barShouldReserveExclusiveZone(const BarConfig& barConfig, bool ipcLayoutReleased, bool contentVisuallyShown) noexcept { + if (ipcLayoutReleased || !barConfig.reserveSpace) { + return false; + } + if (barConfig.isAutoHideEnabled() && barConfig.autoHideReserveSpace) { + return contentVisuallyShown; + } + return true; +} diff --git a/src/shell/settings/settings_content_common.cpp b/src/shell/settings/settings_content_common.cpp index 530bc4fb40..4bf38f1040 100644 --- a/src/shell/settings/settings_content_common.cpp +++ b/src/shell/settings/settings_content_common.cpp @@ -45,6 +45,9 @@ namespace settings { if (key == "smart_auto_hide") { return override->smartAutoHide.has_value(); } + if (key == "auto_hide_reserve_space") { + return override->autoHideReserveSpace.has_value(); + } if (key == "show_on_workspace_switch") { return override->showOnWorkspaceSwitch.has_value(); } diff --git a/src/shell/settings/settings_registry.cpp b/src/shell/settings/settings_registry.cpp index e38e241756..6ec7e8cce7 100644 --- a/src/shell/settings/settings_registry.cpp +++ b/src/shell/settings/settings_registry.cpp @@ -3016,6 +3016,19 @@ namespace settings { tr("settings.schema.bar.reserve-space.description"), path("reserve_space"), ToggleSetting{bar.reserveSpace}, "exclusive zone" )); + const SettingVisibility reserveSpaceOn = [barName = bar.name](const Config& c) { + const BarConfig* b = findBar(c, barName); + return b != nullptr && b->reserveSpace; + }; + { + auto e = makeEntry( + section, "general", tr("settings.schema.bar.auto-hide-reserve-space.label"), + tr("settings.schema.bar.auto-hide-reserve-space.description"), path("auto_hide_reserve_space"), + ToggleSetting{bar.autoHideReserveSpace}, "autohide immersive exclusive zone" + ); + e.visibleWhen = reserveSpaceOn; + entries.push_back(std::move(e)); + } entries.push_back(makeEntry( section, "general", tr("settings.schema.bar.layer.label"), tr("settings.schema.bar.layer.description"), path("layer"), @@ -3339,6 +3352,24 @@ namespace settings { tr("settings.schema.bar.reserve-space.description"), monitorPath("reserve_space"), ToggleSetting{ovr.reserveSpace.value_or(bar.reserveSpace)}, "exclusive zone" )); + const SettingVisibility monitorReserveSpaceOn = [barName = bar.name, match = ovr.match](const Config& c) { + const BarConfig* b = findBar(c, barName); + if (b == nullptr) { + return false; + } + const BarMonitorOverride* o = findMonitorOverride(*b, match); + return o != nullptr ? o->reserveSpace.value_or(b->reserveSpace) : b->reserveSpace; + }; + { + auto e = makeEntry( + section, "general", tr("settings.schema.bar.auto-hide-reserve-space.label"), + tr("settings.schema.bar.auto-hide-reserve-space.description"), monitorPath("auto_hide_reserve_space"), + ToggleSetting{ovr.autoHideReserveSpace.value_or(bar.autoHideReserveSpace)}, + "autohide immersive exclusive zone" + ); + e.visibleWhen = monitorReserveSpaceOn; + entries.push_back(std::move(e)); + } entries.push_back(makeEntry( section, "general", tr("settings.schema.bar.layer.label"), tr("settings.schema.bar.layer.description"), monitorPath("layer"), diff --git a/tests/bar_exclusive_zone_test.cpp b/tests/bar_exclusive_zone_test.cpp new file mode 100644 index 0000000000..98f4b08867 --- /dev/null +++ b/tests/bar_exclusive_zone_test.cpp @@ -0,0 +1,74 @@ +#include "shell/bar/bar_reserved_zone.h" + +#include + +namespace { + + bool check(bool cond, const char* msg) { + if (!cond) { + std::cerr << "FAIL: " << msg << '\n'; + } + return cond; + } + + BarConfig autoHideBar() { + BarConfig cfg; + cfg.reserveSpace = true; + cfg.autoHide = true; + return cfg; + } + + BarConfig smartHideBar() { + BarConfig cfg; + cfg.reserveSpace = true; + cfg.smartAutoHide = true; + return cfg; + } + +} // namespace + +int main() { + bool ok = true; + + BarConfig alwaysOn; + alwaysOn.reserveSpace = true; + ok &= check(barShouldReserveExclusiveZone(alwaysOn, false, false), "always-on bar reserves even when not shown"); + ok &= check(barShouldReserveExclusiveZone(alwaysOn, false, true), "always-on bar reserves when shown"); + + auto autoHide = autoHideBar(); + ok &= check(barShouldReserveExclusiveZone(autoHide, false, false), "auto-hide hidden still reserves space by default"); + ok &= check(barShouldReserveExclusiveZone(autoHide, false, true), "auto-hide shown still reserves space by default"); + ok &= check(!barShouldReserveExclusiveZone(autoHide, true, true), "ipc hide releases reserve space"); + + autoHide.autoHideReserveSpace = true; + ok &= check( + !barShouldReserveExclusiveZone(autoHide, false, false), + "auto-hide with reserve-while-visible releases space when hidden" + ); + ok &= check( + barShouldReserveExclusiveZone(autoHide, false, true), + "auto-hide with reserve-while-visible reserves space when shown" + ); + + auto smartHide = smartHideBar(); + ok &= check( + barShouldReserveExclusiveZone(smartHide, false, false), "smart auto-hide hidden keeps static reserve by default" + ); + ok &= check(barShouldReserveExclusiveZone(smartHide, false, true), "smart auto-hide shown reserves space by default"); + + smartHide.autoHideReserveSpace = true; + ok &= check( + !barShouldReserveExclusiveZone(smartHide, false, false), + "smart auto-hide with reserve-while-visible releases space when hidden" + ); + ok &= check( + barShouldReserveExclusiveZone(smartHide, false, true), + "smart auto-hide with reserve-while-visible reserves space when shown" + ); + + BarConfig noReserve = smartHideBar(); + noReserve.reserveSpace = false; + ok &= check(!barShouldReserveExclusiveZone(noReserve, false, true), "reserve_space off never reserves"); + + return ok ? 0 : 1; +} diff --git a/tests/config_schema_roundtrip_test.cpp b/tests/config_schema_roundtrip_test.cpp index 4f9933e59e..e9d31556bc 100644 --- a/tests/config_schema_roundtrip_test.cpp +++ b/tests/config_schema_roundtrip_test.cpp @@ -198,6 +198,7 @@ location = "https://example.invalid/bad" bar.enabled = false; bar.autoHide = true; bar.smartAutoHide = false; + bar.autoHideReserveSpace = false; bar.showOnWorkspaceSwitch = true; bar.reserveSpace = false; bar.layer = "overlay"; @@ -269,6 +270,7 @@ location = "https://example.invalid/bad" ovr.enabled = true; ovr.autoHide = false; ovr.smartAutoHide = false; + ovr.autoHideReserveSpace = false; ovr.showOnWorkspaceSwitch = true; ovr.reserveSpace = true; ovr.layer = "top"; @@ -984,6 +986,7 @@ int main() { [default] auto_hide = true +auto_hide_reserve_space = false background_opacity = 0.85000002384185791 border = "#123456" border_width = 2.0 @@ -1042,6 +1045,7 @@ widget_spacing = 8 [default.monitor.DP-1] auto_hide = false + auto_hide_reserve_space = false background_opacity = 0.69999998807907104 border = "#A1A2A3" border_width = 3.0