Files
noctalia-shell/src/shell/bar/bar.cpp
T

1854 lines
70 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "shell/bar/bar.h"
#include "config/config_service.h"
#include "core/log.h"
#include "core/ui_phase.h"
#include "dbus/power/power_profiles_service.h"
#include "dbus/tray/tray_service.h"
#include "dbus/upower/upower_service.h"
#include "ipc/ipc_service.h"
#include "render/render_context.h"
#include "shell/bar/widget.h"
#include "shell/panel/panel_manager.h"
#include "shell/surface_shadow.h"
#include "system/night_light_manager.h"
#include "system/system_monitor_service.h"
#include "system/weather_service.h"
#include "theme/theme_service.h"
#include "time/time_service.h"
#include "ui/controls/box.h"
#include "ui/controls/flex.h"
#include "ui/palette.h"
#include "ui/style.h"
#include "wayland/wayland_connection.h"
#include <algorithm>
#include <cmath>
#include <linux/input-event-codes.h>
#include <wayland-client-core.h>
namespace {
constexpr float kCircularCapsuleNarrowWidthEpsilon = 1.0f;
constexpr float kAutoHideSlideExtraPx = 16.0f;
constexpr std::int32_t kAutoHideTriggerRegionPx = 4;
bool pointInsideNode(const Node* node, float sceneX, float sceneY) {
if (node == nullptr) {
return false;
}
float localX = 0.0f;
float localY = 0.0f;
if (!Node::mapFromScene(node, sceneX, sceneY, localX, localY)) {
return false;
}
return localX >= 0.0f && localX < node->width() && localY >= 0.0f && localY < node->height();
}
std::pair<float, float> surfaceOriginForOutputLocal(const BarInstance& instance, const WaylandOutput& outputInfo) {
if (instance.surface == nullptr) {
return {0.0f, 0.0f};
}
const auto* surface = instance.surface.get();
const std::uint32_t anchor = surface->anchor();
const bool aTop = (anchor & LayerShellAnchor::Top) != 0;
const bool aBottom = (anchor & LayerShellAnchor::Bottom) != 0;
const bool aLeft = (anchor & LayerShellAnchor::Left) != 0;
const bool aRight = (anchor & LayerShellAnchor::Right) != 0;
const float mTop = static_cast<float>(surface->marginTop());
const float mRight = static_cast<float>(surface->marginRight());
const float mBottom = static_cast<float>(surface->marginBottom());
const float mLeft = static_cast<float>(surface->marginLeft());
const float surfW = static_cast<float>(surface->width());
const float surfH = static_cast<float>(surface->height());
const float outputW = static_cast<float>(outputInfo.logicalWidth);
const float outputH = static_cast<float>(outputInfo.logicalHeight);
float x = 0.0f;
float y = 0.0f;
if (aLeft && aRight) {
x = mLeft;
} else if (aRight) {
x = std::max(0.0f, outputW - mRight - surfW);
} else {
x = mLeft;
}
if (aTop && aBottom) {
y = mTop;
} else if (aBottom) {
y = std::max(0.0f, outputH - mBottom - surfH);
} else {
y = mTop;
}
return {x, y};
}
std::uint32_t positionToAnchor(const std::string& position) {
if (position == "bottom") {
return LayerShellAnchor::Bottom | LayerShellAnchor::Left | LayerShellAnchor::Right;
}
if (position == "left") {
return LayerShellAnchor::Top | LayerShellAnchor::Bottom | LayerShellAnchor::Left;
}
if (position == "right") {
return LayerShellAnchor::Top | LayerShellAnchor::Bottom | LayerShellAnchor::Right;
}
// Default: top
return LayerShellAnchor::Top | LayerShellAnchor::Left | LayerShellAnchor::Right;
}
constexpr Logger kLog("bar");
ColorSpec withOpacity(const ColorSpec& color, float opacity) {
ColorSpec out = color;
out.alpha = std::clamp(out.alpha * std::clamp(opacity, 0.0f, 1.0f), 0.0f, 1.0f);
return out;
}
struct BarVisualGeometry {
float x = 0.0f;
float y = 0.0f;
float width = 0.0f;
float height = 0.0f;
};
// Returns true when two bar configs would produce an identical layer-shell
// surface (same anchor, size, exclusive zone, namespace). When true, an
// existing BarInstance can be retained on reload and only its widget tree
// rebuilt — avoiding the screen-shift caused by destroying and recreating
// the exclusive zone.
bool barConfigSurfaceFieldsEqual(const BarConfig& a, const BarConfig& b,
const ShellConfig::ShadowConfig& previousShadow,
const ShellConfig::ShadowConfig& nextShadow) {
const bool sameShadowSurface =
(!a.shadow && !b.shadow) || shell::surface_shadow::sameSurfaceMetrics(previousShadow, nextShadow);
return a.name == b.name && a.position == b.position && a.enabled == b.enabled && a.autoHide == b.autoHide &&
a.reserveSpace == b.reserveSpace && a.thickness == b.thickness && a.marginEnds == b.marginEnds &&
a.marginEdge == b.marginEdge && a.shadow == b.shadow && sameShadowSurface &&
a.monitorOverrides == b.monitorOverrides;
}
bool barSurfaceOrderRequiresRecreate(const std::vector<BarConfig>& previous, const std::vector<BarConfig>& next) {
std::vector<std::string> preserved;
preserved.reserve(previous.size());
for (const auto& oldBar : previous) {
const auto it =
std::find_if(next.begin(), next.end(), [&](const BarConfig& newBar) { return newBar.name == oldBar.name; });
if (it != next.end()) {
preserved.push_back(oldBar.name);
}
}
if (preserved.size() > next.size()) {
return true;
}
for (std::size_t i = 0; i < preserved.size(); ++i) {
if (next[i].name != preserved[i]) {
return true;
}
}
return false;
}
BarVisualGeometry computeBarVisualGeometry(const BarConfig& cfg, const ShellConfig::ShadowConfig& shadow,
float surfaceWidth, float surfaceHeight) {
const float barThickness = static_cast<float>(cfg.thickness);
const float marginEnds = static_cast<float>(cfg.marginEnds);
const float marginEdge = static_cast<float>(cfg.marginEdge);
const bool isBottom = cfg.position == "bottom";
const bool isRight = cfg.position == "right";
const bool isVertical = (cfg.position == "left" || cfg.position == "right");
const auto sbi = shell::surface_shadow::bleed(cfg.shadow, shadow);
const float bleedLeft = static_cast<float>(sbi.left);
const float bleedRight = static_cast<float>(sbi.right);
const float bleedUp = static_cast<float>(sbi.up);
const float bleedDown = static_cast<float>(sbi.down);
if (isVertical) {
// Vertical bar: edge gap is left/right, ends inset is top/bottom.
const float x = isRight ? bleedLeft : std::min(marginEdge, bleedLeft);
const float y = std::min(marginEnds, bleedUp);
return {
.x = x,
.y = y,
.width = barThickness,
.height = surfaceHeight - y - std::min(marginEnds, bleedDown),
};
}
// Horizontal bar: edge gap is top/bottom, ends inset is left/right.
const float x = std::min(marginEnds, bleedLeft);
const float y = isBottom ? bleedUp : std::min(marginEdge, bleedUp);
return {
.x = x,
.y = y,
.width = surfaceWidth - x - std::min(marginEnds, bleedRight),
.height = barThickness,
};
}
std::pair<float, float> computeAutoHideHiddenDelta(bool isVertical, bool isBottom, bool isRight, float w, float h,
float contentLeft, float contentTop, float contentRight,
float contentBottom) {
const float k = kAutoHideSlideExtraPx;
if (!isVertical) {
if (isBottom) {
return {0.0f, (h - contentTop) + k};
}
return {0.0f, -(contentBottom + k)};
}
if (isRight) {
return {(w - contentLeft) + k, 0.0f};
}
return {-(contentRight + k), 0.0f};
}
void applyBarShadowStyle(BarInstance& instance, const ShellConfig::ShadowConfig& shadowConfig, float surfaceWidth,
float surfaceHeight) {
if (instance.shadow == nullptr) {
return;
}
const Radii barRadii{
static_cast<float>(instance.barConfig.radiusTopLeft),
static_cast<float>(instance.barConfig.radiusTopRight),
static_cast<float>(instance.barConfig.radiusBottomRight),
static_cast<float>(instance.barConfig.radiusBottomLeft),
};
const auto barVisual = computeBarVisualGeometry(instance.barConfig, shadowConfig, surfaceWidth, surfaceHeight);
const float barAreaW = barVisual.width;
const float barAreaH = barVisual.height;
const float bgOpacity = std::clamp(instance.barConfig.backgroundOpacity, 0.0f, 1.0f);
const float shadowX = barVisual.x + static_cast<float>(shadowConfig.offsetX);
const float shadowY = barVisual.y + static_cast<float>(shadowConfig.offsetY);
RoundedRectStyle shadowStyle =
shell::surface_shadow::style(shadowConfig, bgOpacity, shell::surface_shadow::Shape{.radius = barRadii});
const bool panelShadowExclusion = instance.attachedPanelGeometry.has_value() &&
instance.attachedPanelGeometry->width > 0.0f &&
instance.attachedPanelGeometry->height > 0.0f;
if (panelShadowExclusion) {
const auto& attached = *instance.attachedPanelGeometry;
const float convexRadius = std::max(0.0f, attached.cornerRadius);
const float bulgeRadius = std::max(0.0f, attached.bulgeRadius);
const std::string_view barPosition = instance.barConfig.position;
const auto corners = attached_panel::cornerShapes(barPosition);
const auto pickRadius = [&](CornerShape shape) {
return shape == CornerShape::Concave ? bulgeRadius : convexRadius;
};
shadowStyle.shadowExclusion = true;
shadowStyle.shadowExclusionOffsetX = shadowX - attached.x;
shadowStyle.shadowExclusionOffsetY = shadowY - attached.y;
shadowStyle.shadowExclusionWidth = attached.width;
shadowStyle.shadowExclusionHeight = attached.height;
shadowStyle.shadowExclusionCorners = corners;
shadowStyle.shadowExclusionLogicalInset = attached_panel::logicalInset(barPosition, bulgeRadius);
shadowStyle.shadowExclusionRadius =
Radii{pickRadius(corners.tl), pickRadius(corners.tr), pickRadius(corners.br), pickRadius(corners.bl)};
}
auto configureShadow = [&](Box* node, float x, float y) {
if (node == nullptr) {
return;
}
node->setStyle(shadowStyle);
node->setZIndex(-1);
node->setPosition(x, y);
node->setSize(barAreaW, barAreaH);
};
instance.shadow->setVisible(true);
configureShadow(instance.shadow, shadowX, shadowY);
if (instance.shadowLeftClip != nullptr) {
instance.shadowLeftClip->setVisible(false);
}
if (instance.shadowRightClip != nullptr) {
instance.shadowRightClip->setVisible(false);
}
}
void layoutBarSections(BarInstance& instance, Renderer& renderer, float barAreaW, float barAreaH, float padding,
bool isVertical) {
const float slotCross = isVertical ? barAreaW : barAreaH;
auto layoutWidgets = [&](std::vector<std::unique_ptr<Widget>>& widgets) {
for (auto& widget : widgets) {
if (widget->root() != nullptr) {
widget->layout(renderer, barAreaW, barAreaH);
}
}
};
layoutWidgets(instance.startWidgets);
layoutWidgets(instance.centerWidgets);
layoutWidgets(instance.endWidgets);
float cachedBodyExtent = -1.0f;
float cachedBodyExtentScale = -1.0f;
auto finalizeCapsules = [isVertical, slotCross, &renderer, &cachedBodyExtent,
&cachedBodyExtentScale](std::vector<BarCapsuleRun>& runs) {
for (auto& run : runs) {
Node* shell = run.shell;
Box* bg = run.bg;
Node* content = run.content;
if (shell == nullptr || bg == nullptr || content == nullptr) {
continue;
}
if (run.container != nullptr) {
run.container->layout(renderer);
}
bool hasVisibleContent = false;
bool hasVisibleInk = false;
for (Widget* widget : run.widgets) {
if (widget == nullptr || widget->root() == nullptr) {
continue;
}
hasVisibleContent = hasVisibleContent || widget->root()->visible();
hasVisibleInk = hasVisibleInk || widget->shouldShowBarCapsule();
}
shell->setVisible(hasVisibleContent);
const float scale = run.contentScale;
const float iw = content->width();
const float ih = content->height();
if (!hasVisibleInk) {
shell->setSize(iw, ih);
content->setPosition(0.0f, 0.0f);
bg->setVisible(false);
bg->setPosition(0.0f, 0.0f);
bg->setSize(iw, ih);
continue;
}
if (scale != cachedBodyExtentScale) {
const auto refMetrics = renderer.measureFont(Style::fontSizeBody * scale);
cachedBodyExtent = std::round(refMetrics.bottom - refMetrics.top);
cachedBodyExtentScale = scale;
}
const float bodyExtent = cachedBodyExtent;
const float iconExtent = std::max(bodyExtent, std::round(Style::barGlyphSize * scale));
const float pad = run.spec.padding * scale;
const float padMain = pad;
const float padCross = std::min(pad, Style::spaceXs * scale);
float capsuleCross = bodyExtent + 2.0f * padCross;
if (isVertical) {
capsuleCross = std::min(capsuleCross, slotCross);
}
float shellMain = (isVertical ? ih : iw) + 2.0f * padMain;
float shellCross = capsuleCross;
float shellW = isVertical ? shellCross : shellMain;
float shellH = isVertical ? shellMain : shellCross;
float contentX = std::round((shellW - iw) * 0.5f);
float contentY = std::round((shellH - ih) * 0.5f);
// Glyph-only widgets become a fixed circle based on the bar capsule
// cross-size, not on the measured content width. Multi-line / wide
// content (e.g. stacked vertical clock) must NOT be squared, or the
// capsule collapses on the main axis.
const float iconThreshold = iconExtent + (kCircularCapsuleNarrowWidthEpsilon * scale);
const bool iconSized = run.allowCircularSizing && iw <= iconThreshold && ih <= iconThreshold;
if (iconSized) {
const float side = capsuleCross;
shellW = side;
shellH = side;
contentX = std::round((shellW - iw) * 0.5f);
contentY = std::round((shellH - ih) * 0.5f);
}
shell->setSize(shellW, shellH);
bg->setVisible(true);
bg->setPosition(0.0f, 0.0f);
bg->setSize(shellW, shellH);
content->setPosition(contentX, contentY);
bg->setRadius(std::min(shellW, shellH) * 0.5f);
}
};
finalizeCapsules(instance.startCapsuleRuns);
finalizeCapsules(instance.centerCapsuleRuns);
finalizeCapsules(instance.endCapsuleRuns);
const float contentMainStart = padding;
const float contentMainEnd = std::max(contentMainStart, (isVertical ? barAreaH : barAreaW) - padding);
const float contentMainSpan = std::max(0.0f, contentMainEnd - contentMainStart);
auto configureSlot = [&](Node* slot, float mainOffset, float mainSize) {
slot->setClipChildren(true);
if (isVertical) {
slot->setPosition(0.0f, mainOffset);
slot->setSize(slotCross, mainSize);
} else {
slot->setPosition(mainOffset, 0.0f);
slot->setSize(mainSize, slotCross);
}
};
auto configureSection = [&](Flex* section, FlexJustify justify) {
section->setJustify(justify);
section->layout(renderer);
};
configureSection(instance.startSection, FlexJustify::Start);
configureSection(instance.centerSection, FlexJustify::Center);
configureSection(instance.endSection, FlexJustify::End);
// Anchor mode: if a center widget is flagged as the anchor, pin its center to the
// bar midline so surrounding siblings growing/shrinking cannot drift it sideways.
const Node* anchorNode = nullptr;
for (const auto& widget : instance.centerWidgets) {
if (widget != nullptr && widget->isAnchor() && widget->layoutBoundsNode() != nullptr) {
anchorNode = widget->layoutBoundsNode();
break;
}
}
const float barMidline = contentMainStart + contentMainSpan * 0.5f;
const float centerNaturalMain = isVertical ? instance.centerSection->height() : instance.centerSection->width();
float centerSlotStart;
float centerSlotMain;
float centerSectionOffset; // offset of section origin within its slot along main axis
if (anchorNode != nullptr) {
const float anchorOffsetInSection = isVertical ? anchorNode->y() : anchorNode->x();
const float anchorSpan = isVertical ? anchorNode->height() : anchorNode->width();
const float anchorCenterInSection = anchorOffsetInSection + anchorSpan * 0.5f;
// Place the section so that the anchor's center sits at barMidline.
float desiredSectionStart = barMidline - anchorCenterInSection;
// Clamp so the section stays within the content area.
const float maxStart = contentMainEnd - centerNaturalMain;
desiredSectionStart = std::clamp(desiredSectionStart, contentMainStart, std::max(contentMainStart, maxStart));
centerSlotStart = desiredSectionStart;
centerSlotMain = std::min(centerNaturalMain, contentMainEnd - centerSlotStart);
centerSectionOffset = 0.0f;
} else {
centerSlotMain = std::min(contentMainSpan, centerNaturalMain);
centerSlotStart = contentMainStart + std::max(0.0f, (contentMainSpan - centerSlotMain) * 0.5f);
centerSectionOffset = (centerSlotMain - centerNaturalMain) * 0.5f;
}
const float centerSlotEnd = centerSlotStart + centerSlotMain;
const float startSlotMain = std::max(0.0f, centerSlotStart - contentMainStart);
const float endSlotMain = std::max(0.0f, contentMainEnd - centerSlotEnd);
configureSlot(instance.startSlot, contentMainStart, startSlotMain);
configureSlot(instance.centerSlot, centerSlotStart, centerSlotMain);
configureSlot(instance.endSlot, centerSlotEnd, endSlotMain);
if (isVertical) {
instance.startSection->setPosition((slotCross - instance.startSection->width()) * 0.5f, 0.0f);
instance.centerSection->setPosition((slotCross - instance.centerSection->width()) * 0.5f, centerSectionOffset);
instance.endSection->setPosition((slotCross - instance.endSection->width()) * 0.5f,
endSlotMain - instance.endSection->height());
} else {
instance.startSection->setPosition(0.0f, (slotCross - instance.startSection->height()) * 0.5f);
instance.centerSection->setPosition(centerSectionOffset, (slotCross - instance.centerSection->height()) * 0.5f);
instance.endSection->setPosition(endSlotMain - instance.endSection->width(),
(slotCross - instance.endSection->height()) * 0.5f);
}
}
void tickWidgets(std::vector<std::unique_ptr<Widget>>& widgets, float deltaMs) {
for (auto& widget : widgets) {
if (widget != nullptr && widget->needsFrameTick()) {
widget->onFrameTick(deltaMs);
}
}
}
bool widgetsNeedFrameTick(const std::vector<std::unique_ptr<Widget>>& widgets) {
return std::any_of(widgets.begin(), widgets.end(),
[](const auto& widget) { return widget != nullptr && widget->needsFrameTick(); });
}
} // namespace
Bar::Bar() = default;
bool Bar::initialize(WaylandConnection& wayland, ConfigService* config, TimeService* timeService,
NotificationManager* notifications, TrayService* tray, PipeWireService* audio,
UPowerService* upower, SystemMonitorService* sysmon, PowerProfilesService* powerProfiles,
NetworkService* network, IdleInhibitor* idleInhibitor, MprisService* mpris,
PipeWireSpectrum* audioSpectrum, HttpClient* httpClient, WeatherService* weatherService,
RenderContext* renderContext, NightLightManager* nightLight,
noctalia::theme::ThemeService* themeService, BluetoothService* bluetooth,
BrightnessService* brightness, LockKeysService* lockKeys, FileWatcher* fileWatcher) {
m_wayland = &wayland;
m_config = config;
m_notifications = notifications;
m_tray = tray;
m_audio = audio;
m_upower = upower;
m_sysmon = sysmon;
m_powerProfiles = powerProfiles;
m_network = network;
m_idleInhibitor = idleInhibitor;
m_mpris = mpris;
m_audioSpectrum = audioSpectrum;
m_httpClient = httpClient;
m_weatherService = weatherService;
m_renderContext = renderContext;
m_nightLight = nightLight;
m_themeService = themeService;
m_bluetooth = bluetooth;
m_brightness = brightness;
m_lockKeys = lockKeys;
m_fileWatcher = fileWatcher;
m_widgetFactory = std::make_unique<WidgetFactory>(
*m_wayland, m_config->config(), m_notifications, m_tray, m_audio, m_upower, m_sysmon, m_powerProfiles, m_network,
m_idleInhibitor, m_mpris, m_audioSpectrum, m_httpClient, m_weatherService, m_nightLight, m_themeService,
m_bluetooth, m_brightness, m_lockKeys, m_fileWatcher);
if (timeService != nullptr) {
timeService->setTickSecondCallback([this]() {
for (auto& inst : m_instances) {
if (inst->surface != nullptr) {
inst->surface->requestUpdate();
}
}
});
}
m_lastBars = m_config->config().bars;
m_lastWidgets = m_config->config().widgets;
m_lastShadow = m_config->config().shell.shadow;
m_config->addReloadCallback([this]() {
const auto& cfg = m_config->config();
if (cfg.bars == m_lastBars && cfg.widgets == m_lastWidgets && cfg.shell.shadow == m_lastShadow) {
return;
}
reload();
});
syncInstances();
return true;
}
void Bar::onSecondTick() {
for (auto& inst : m_instances) {
if (inst->surface != nullptr) {
inst->surface->requestUpdate();
}
}
}
void Bar::reload() {
kLog.info("reloading config");
const auto previousBars = m_lastBars;
const auto previousShadow = m_lastShadow;
const bool recreateForOrder = barSurfaceOrderRequiresRecreate(previousBars, m_config->config().bars);
m_lastBars = m_config->config().bars;
m_lastWidgets = m_config->config().widgets;
m_lastShadow = m_config->config().shell.shadow;
m_widgetFactory = std::make_unique<WidgetFactory>(
*m_wayland, m_config->config(), m_notifications, m_tray, m_audio, m_upower, m_sysmon, m_powerProfiles, m_network,
m_idleInhibitor, m_mpris, m_audioSpectrum, m_httpClient, m_weatherService, m_nightLight, m_themeService,
m_bluetooth, m_brightness, m_lockKeys, m_fileWatcher);
if (recreateForOrder) {
kLog.info("bar order changed; recreating layer-shell surfaces");
closeAllInstances();
wl_display_roundtrip(m_wayland->display());
syncInstances();
return;
}
// Look up new bar configs by name.
std::unordered_map<std::string, std::pair<const BarConfig*, std::size_t>> newBarsByName;
newBarsByName.reserve(m_lastBars.size());
for (std::size_t i = 0; i < m_lastBars.size(); ++i) {
newBarsByName[m_lastBars[i].name] = {&m_lastBars[i], i};
}
// For each existing instance, decide whether to rebuild contents in place
// (surface preserved → no exclusive-zone churn) or destroy (will be recreated
// by syncInstances below).
bool destroyedAny = false;
std::erase_if(m_instances, [&](const std::unique_ptr<BarInstance>& instUp) {
auto& inst = *instUp;
auto it = newBarsByName.find(inst.barConfig.name);
auto destroy = [&]() {
if (inst.surface != nullptr) {
m_surfaceMap.erase(inst.surface->wlSurface());
}
if (m_hoveredInstance == &inst) {
m_hoveredInstance = nullptr;
}
destroyedAny = true;
return true;
};
if (it == newBarsByName.end()) {
return destroy();
}
const auto& outputs = m_wayland->outputs();
auto outIt =
std::find_if(outputs.begin(), outputs.end(), [&inst](const auto& o) { return o.name == inst.outputName; });
if (outIt == outputs.end()) {
return destroy();
}
auto resolved = ConfigService::resolveForOutput(*it->second.first, *outIt);
if (!resolved.enabled) {
return destroy();
}
if (!barConfigSurfaceFieldsEqual(inst.barConfig, resolved, previousShadow, m_lastShadow)) {
return destroy();
}
inst.barIndex = it->second.second;
rebuildInstanceContents(inst, resolved);
return false;
});
if (destroyedAny) {
// Drain pending Wayland events for the just-destroyed surfaces before
// creating new ones. Without this, the roundtrip inside LayerSurface::initialize
// reads stale closures for dead proxies, which libwayland drops without freeing.
wl_display_roundtrip(m_wayland->display());
}
syncInstances();
}
void Bar::closeAllInstances() {
m_surfaceMap.clear();
m_hoveredInstance = nullptr;
m_instances.clear();
}
void Bar::onOutputChange() { syncInstances(); }
void Bar::refresh() {
for (auto& inst : m_instances) {
if (inst->surface != nullptr) {
inst->surface->requestUpdate();
if (inst->animations.hasActive() || instanceNeedsFrameTick(*inst)) {
inst->surface->requestRedraw();
}
}
}
}
void Bar::requestRedraw() {
for (auto& inst : m_instances) {
if (inst->surface != nullptr) {
inst->surface->requestRedraw();
}
}
}
void Bar::requestLayout() {
for (auto& inst : m_instances) {
if (inst->surface != nullptr) {
inst->surface->requestLayout();
}
}
}
void Bar::setAutoHideSuppressionCallback(std::function<bool()> callback) {
m_autoHideSuppressionCallback = std::move(callback);
}
bool Bar::isRunning() const noexcept {
if (m_forceHidden) {
return true; // hidden but still alive — do not exit the main loop
}
return std::any_of(m_instances.begin(), m_instances.end(),
[](const auto& inst) { return inst->surface && inst->surface->isRunning(); });
}
std::optional<LayerPopupParentContext> Bar::popupParentContextForSurface(wl_surface* surface) const noexcept {
auto* instance = instanceForSurface(surface);
if (instance == nullptr || instance->surface == nullptr) {
return std::nullopt;
}
auto* layerSurface = instance->surface->layerSurface();
const auto width = instance->surface->width();
const auto height = instance->surface->height();
if (layerSurface == nullptr || width == 0 || height == 0) {
return std::nullopt;
}
return LayerPopupParentContext{
.surface = instance->surface->wlSurface(),
.layerSurface = layerSurface,
.output = instance->output,
.width = width,
.height = height,
};
}
std::optional<LayerPopupParentContext> Bar::preferredPopupParentContext(wl_output* output) const noexcept {
BarInstance* instance = instanceForOutput(output);
if (instance == nullptr && !m_instances.empty()) {
instance = m_instances.front().get();
}
return instance != nullptr && instance->surface != nullptr
? popupParentContextForSurface(instance->surface->wlSurface())
: std::nullopt;
}
std::vector<InputRect> Bar::surfaceRectsForOutput(wl_output* output) const {
std::vector<InputRect> rects;
if (m_wayland == nullptr || output == nullptr) {
return rects;
}
const WaylandOutput* wlOutput = m_wayland->findOutputByWl(output);
if (wlOutput == nullptr) {
return rects;
}
// logicalWidth/Height become valid only after xdg_output.done; before that
// we cannot accurately place a bottom/right anchored bar.
if (wlOutput->logicalWidth <= 0 || wlOutput->logicalHeight <= 0) {
return rects;
}
const std::int32_t outputW = wlOutput->logicalWidth;
const std::int32_t outputH = wlOutput->logicalHeight;
for (const auto& instance : m_instances) {
if (instance == nullptr || instance->output != output || instance->surface == nullptr) {
continue;
}
const auto* surface = instance->surface.get();
const std::uint32_t anchor = surface->anchor();
const bool aTop = (anchor & LayerShellAnchor::Top) != 0;
const bool aBottom = (anchor & LayerShellAnchor::Bottom) != 0;
const bool aLeft = (anchor & LayerShellAnchor::Left) != 0;
const bool aRight = (anchor & LayerShellAnchor::Right) != 0;
const std::int32_t mTop = surface->marginTop();
const std::int32_t mRight = surface->marginRight();
const std::int32_t mBottom = surface->marginBottom();
const std::int32_t mLeft = surface->marginLeft();
// surface->width()/height() may be 0 before configure; fall back to BarConfig
// thickness so we still publish a sensible exclusion for fresh surfaces.
const std::int32_t surfW = static_cast<std::int32_t>(surface->width());
const std::int32_t surfH = static_cast<std::int32_t>(surface->height());
std::int32_t rectW = surfW;
std::int32_t rectH = surfH;
std::int32_t rectX = 0;
std::int32_t rectY = 0;
if (aLeft && aRight) {
rectW = std::max(0, outputW - mLeft - mRight);
rectX = mLeft;
} else if (aRight) {
rectX = std::max(0, outputW - mRight - rectW);
} else {
rectX = mLeft;
}
if (aTop && aBottom) {
rectH = std::max(0, outputH - mTop - mBottom);
rectY = mTop;
} else if (aBottom) {
rectY = std::max(0, outputH - mBottom - rectH);
} else {
rectY = mTop;
}
if (rectW > 0 && rectH > 0) {
rects.push_back(InputRect{rectX, rectY, rectW, rectH});
}
}
return rects;
}
std::vector<wl_surface*> Bar::allBarSurfaces() const {
std::vector<wl_surface*> surfaces;
surfaces.reserve(m_instances.size());
for (const auto& instance : m_instances) {
if (instance != nullptr && instance->surface != nullptr) {
if (wl_surface* s = instance->surface->wlSurface(); s != nullptr) {
surfaces.push_back(s);
}
}
}
return surfaces;
}
void Bar::setAttachedPanelGeometry(wl_output* output, std::optional<AttachedPanelGeometry> geometry) {
BarInstance* instance = instanceForOutput(output);
if (instance == nullptr) {
return;
}
instance->attachedPanelGeometry = geometry;
if (instance->surface != nullptr && instance->surface->width() > 0 && instance->surface->height() > 0) {
applyBarShadowStyle(*instance, m_config->config().shell.shadow, static_cast<float>(instance->surface->width()),
static_cast<float>(instance->surface->height()));
instance->surface->requestRedraw();
}
}
void Bar::beginAttachedPopup(wl_surface* surface) {
auto* instance = instanceForSurface(surface);
if (instance == nullptr) {
return;
}
++instance->attachedPopupCount;
}
void Bar::endAttachedPopup(wl_surface* surface) {
auto* instance = instanceForSurface(surface);
if (instance == nullptr) {
return;
}
if (instance->attachedPopupCount > 0) {
--instance->attachedPopupCount;
}
if (m_wayland != nullptr) {
instance->pointerInside = (m_wayland->lastPointerSurface() == surface);
}
if (!instance->pointerInside && m_hoveredInstance == instance) {
m_hoveredInstance = nullptr;
} else if (instance->pointerInside) {
m_hoveredInstance = instance;
}
if (instance->attachedPopupCount > 0 || !instance->barConfig.autoHide || instance->pointerInside) {
return;
}
const bool suppressAutoHide = (m_autoHideSuppressionCallback != nullptr) ? m_autoHideSuppressionCallback() : false;
if (!suppressAutoHide) {
startHideFadeOut(*instance);
}
}
void Bar::show() {
if (!m_forceHidden) {
return;
}
m_forceHidden = false;
syncInstances();
}
void Bar::hide() {
if (m_forceHidden) {
return;
}
m_forceHidden = true;
closeAllInstances();
}
void Bar::syncInstances() {
if (m_forceHidden) {
return;
}
const auto& outputs = m_wayland->outputs();
const auto& bars = m_config->config().bars;
// Remove instances for outputs that no longer exist
std::erase_if(m_instances, [&outputs](const auto& inst) {
bool found =
std::any_of(outputs.begin(), outputs.end(), [&inst](const auto& out) { return out.name == inst->outputName; });
if (!found) {
kLog.info("removing instance for output {}", inst->outputName);
}
return !found;
});
// Create instances for each bar definition × each output
for (std::size_t barIdx = 0; barIdx < bars.size(); ++barIdx) {
for (const auto& output : outputs) {
if (!output.done) {
continue;
}
bool exists = std::any_of(m_instances.begin(), m_instances.end(), [&output, barIdx](const auto& inst) {
return inst->outputName == output.name && inst->barIndex == barIdx;
});
if (!exists) {
auto resolved = ConfigService::resolveForOutput(bars[barIdx], output);
if (!resolved.enabled) {
continue;
}
createInstance(output, barIdx, resolved);
}
}
}
}
void Bar::createInstance(const WaylandOutput& output, std::size_t barIndex, const BarConfig& barConfig) {
auto instance = std::make_unique<BarInstance>();
instance->outputName = output.name;
instance->output = output.output;
instance->scale = output.scale;
instance->barConfig = barConfig;
instance->barIndex = barIndex;
const auto anchor = positionToAnchor(barConfig.position);
const bool vertical = (barConfig.position == "left" || barConfig.position == "right");
const bool isBottom = barConfig.position == "bottom";
const bool isRight = barConfig.position == "right";
const std::int32_t mEnds = barConfig.marginEnds;
const std::int32_t mEdge = barConfig.marginEdge;
const auto sb = shell::surface_shadow::bleed(barConfig.shadow, m_config->config().shell.shadow);
const bool reserveExclusiveZone = barConfig.reserveSpace;
// Compositor margins absorb the visual gap where the shadow doesn't reach.
// The surface is sized to cover only the bar rect plus its shadow footprint.
std::int32_t mLeft = 0, mRight = 0, mTop = 0, mBottom = 0;
std::uint32_t surfW = 0, surfH = 0;
std::int32_t exclusiveZone = reserveExclusiveZone ? 0 : -1;
if (!vertical) {
// Horizontal bar: ends inset = left/right, edge gap = top/bottom.
mLeft = std::max(0, mEnds - sb.left);
mRight = std::max(0, mEnds - sb.right);
if (isBottom) {
mBottom = std::max(0, mEdge - sb.down);
surfH = static_cast<std::uint32_t>(sb.up + barConfig.thickness + std::min(mEdge, sb.down));
if (reserveExclusiveZone) {
exclusiveZone = barConfig.thickness + std::min(mEdge, sb.down);
}
} else {
mTop = std::max(0, mEdge - sb.up);
surfH = static_cast<std::uint32_t>(std::min(mEdge, sb.up) + barConfig.thickness + sb.down);
if (reserveExclusiveZone) {
exclusiveZone = std::min(mEdge, sb.up) + barConfig.thickness;
}
}
} else {
// Vertical bar: ends inset = top/bottom, edge gap = left/right.
mTop = std::max(0, mEnds - sb.up);
mBottom = std::max(0, mEnds - sb.down);
if (isRight) {
mRight = std::max(0, mEdge - sb.right);
surfW = static_cast<std::uint32_t>(sb.left + barConfig.thickness + std::min(mEdge, sb.right));
if (reserveExclusiveZone) {
exclusiveZone = barConfig.thickness + std::min(mEdge, sb.right);
}
} else {
mLeft = std::max(0, mEdge - sb.left);
surfW = static_cast<std::uint32_t>(std::min(mEdge, sb.left) + barConfig.thickness + sb.right);
if (reserveExclusiveZone) {
exclusiveZone = std::min(mEdge, sb.left) + barConfig.thickness;
}
}
}
kLog.info("creating #{} \"{}\" on {} ({}), thickness={} position={} reserve_space={} exclusive_zone={}", barIndex,
barConfig.name, output.connectorName, output.description, barConfig.thickness, barConfig.position,
barConfig.reserveSpace, exclusiveZone);
auto surfaceConfig = LayerSurfaceConfig{
.nameSpace = "noctalia-bar-" + barConfig.name,
.layer = LayerShellLayer::Top,
.anchor = anchor,
.width = surfW,
.height = surfH,
.exclusiveZone = exclusiveZone,
.marginTop = mTop,
.marginRight = mRight,
.marginBottom = mBottom,
.marginLeft = mLeft,
.defaultHeight = surfH,
};
instance->surface = std::make_unique<LayerSurface>(*m_wayland, std::move(surfaceConfig));
instance->surface->setRenderContext(m_renderContext);
auto* inst = instance.get();
instance->surface->setConfigureCallback(
[this, inst](std::uint32_t width, std::uint32_t height) { buildScene(*inst, width, height); });
instance->surface->setPrepareFrameCallback(
[this, inst](bool needsUpdate, bool needsLayout) { prepareFrame(*inst, needsUpdate, needsLayout); });
instance->surface->setFrameTickCallback([inst](float deltaMs) {
tickWidgets(inst->startWidgets, deltaMs);
tickWidgets(inst->centerWidgets, deltaMs);
tickWidgets(inst->endWidgets, deltaMs);
});
instance->surface->setAnimationManager(&instance->animations);
populateWidgets(*instance);
if (!instance->surface->initialize(output.output)) {
kLog.warn("failed to initialize surface for output {}", output.name);
return;
}
m_surfaceMap[instance->surface->wlSurface()] = instance.get();
m_instances.push_back(std::move(instance));
}
void Bar::destroyInstance(std::uint32_t outputName) {
std::erase_if(m_instances, [outputName](const auto& inst) { return inst->outputName == outputName; });
}
void Bar::populateWidgets(BarInstance& instance) {
const auto& widgetConfigs = m_config->config().widgets;
auto createWidgets = [&](const std::vector<std::string>& names, std::vector<std::unique_ptr<Widget>>& dest) {
for (const auto& name : names) {
auto widget =
m_widgetFactory->create(name, instance.output, instance.barConfig.scale, instance.barConfig.position);
if (widget != nullptr) {
const WidgetConfig* wcPtr = nullptr;
if (auto it = widgetConfigs.find(name); it != widgetConfigs.end()) {
wcPtr = &it->second;
widget->setAnchor(wcPtr->getBool("anchor", false));
}
widget->setBarCapsuleSpec(resolveWidgetBarCapsuleSpec(instance.barConfig, wcPtr));
if (wcPtr != nullptr && wcPtr->hasSetting("color")) {
widget->setWidgetForeground(colorSpecFromConfigString(wcPtr->getString("color", "")));
} else if (instance.barConfig.widgetColor.has_value()) {
widget->setWidgetForeground(*instance.barConfig.widgetColor);
}
dest.push_back(std::move(widget));
}
}
};
createWidgets(instance.barConfig.startWidgets, instance.startWidgets);
createWidgets(instance.barConfig.centerWidgets, instance.centerWidgets);
createWidgets(instance.barConfig.endWidgets, instance.endWidgets);
}
void Bar::attachWidgetsToSections(BarInstance& instance) {
const bool isVertical = instance.barConfig.position == "left" || instance.barConfig.position == "right";
const float widgetSpacing = static_cast<float>(instance.barConfig.widgetSpacing);
auto attach = [&](std::vector<std::unique_ptr<Widget>>& widgets, std::vector<BarCapsuleRun>& capsuleRuns,
Flex* section) {
if (section == nullptr) {
return;
}
for (auto& widget : widgets) {
widget->setAnimationManager(&instance.animations);
widget->setUpdateCallback([surface = instance.surface.get()]() {
if (surface != nullptr) {
surface->requestUpdate();
}
});
widget->setRedrawCallback([surface = instance.surface.get()]() {
if (surface != nullptr) {
surface->requestRedraw();
}
});
widget->setFrameTickRequestCallback([surface = instance.surface.get()]() {
if (surface != nullptr) {
surface->requestFrameTick();
}
});
widget->setPanelToggleCallback([this, inst = &instance](std::string_view panelId, std::string_view context,
std::optional<float> anchorSurfaceX,
std::optional<float> anchorSurfaceY) {
float anchorX = inst->lastPointerSx;
float anchorY = inst->lastPointerSy;
if (anchorSurfaceX.has_value()) {
anchorX = *anchorSurfaceX;
}
if (anchorSurfaceY.has_value()) {
anchorY = *anchorSurfaceY;
}
if (m_wayland != nullptr && inst->output != nullptr) {
if (const auto* out = m_wayland->findOutputByWl(inst->output);
out != nullptr && out->logicalWidth > 0 && out->logicalHeight > 0) {
const auto [surfaceX, surfaceY] = surfaceOriginForOutputLocal(*inst, *out);
anchorX += surfaceX;
anchorY += surfaceY;
}
}
PanelManager::instance().togglePanel(
std::string(panelId),
PanelOpenRequest{.output = inst->output,
.anchorX = anchorX,
.anchorY = anchorY,
.hasExplicitAnchor = anchorSurfaceX.has_value() || anchorSurfaceY.has_value(),
.context = context,
.sourceBarName = inst->barConfig.name});
});
widget->create();
}
capsuleRuns.clear();
auto addPlainWidget = [&](Widget& widget) {
widget.setBarCapsuleScene(nullptr, nullptr);
section->addChild(widget.releaseRoot());
};
auto addSingleCapsule = [&](Widget& widget) {
const auto& cap = widget.barCapsuleSpec();
auto shell = std::make_unique<Node>();
Node* shellPtr = shell.get();
auto capsuleBg = std::make_unique<Box>();
Box* bgPtr = capsuleBg.get();
capsuleBg->setFill(withOpacity(cap.fill, cap.opacity));
const float scale = widget.contentScale();
if (cap.border.has_value()) {
capsuleBg->setBorder(*cap.border, Style::borderWidth * scale);
} else {
capsuleBg->clearBorder();
}
capsuleBg->setZIndex(-1);
shellPtr->addChild(std::move(capsuleBg));
shellPtr->addChild(widget.releaseRoot());
widget.setBarCapsuleScene(shellPtr, bgPtr);
capsuleRuns.push_back(BarCapsuleRun{
.shell = shellPtr,
.bg = bgPtr,
.container = nullptr,
.content = widget.root(),
.spec = cap,
.contentScale = widget.contentScale(),
.allowCircularSizing = true,
.widgets = {&widget},
});
section->addChild(std::move(shell));
};
auto canJoinCapsuleGroup = [](const Widget& first, const Widget& next) {
const auto& firstSpec = first.barCapsuleSpec();
const auto& nextSpec = next.barCapsuleSpec();
return firstSpec.enabled && nextSpec.enabled && !first.isAnchor() && !next.isAnchor() &&
!firstSpec.group.empty() && firstSpec == nextSpec && first.contentScale() == next.contentScale();
};
std::size_t index = 0;
while (index < widgets.size()) {
auto& widget = widgets[index];
if (widget->root() == nullptr) {
++index;
continue;
}
const auto& cap = widget->barCapsuleSpec();
if (!cap.enabled) {
addPlainWidget(*widget);
++index;
continue;
}
if (widget->isAnchor() || cap.group.empty()) {
addSingleCapsule(*widget);
++index;
continue;
}
std::size_t runEnd = index + 1;
while (runEnd < widgets.size() && widgets[runEnd] != nullptr && widgets[runEnd]->root() != nullptr &&
canJoinCapsuleGroup(*widget, *widgets[runEnd])) {
++runEnd;
}
if (runEnd - index < 2) {
addSingleCapsule(*widget);
++index;
continue;
}
auto shell = std::make_unique<Node>();
Node* shellPtr = shell.get();
auto capsuleBg = std::make_unique<Box>();
Box* bgPtr = capsuleBg.get();
capsuleBg->setFill(withOpacity(cap.fill, cap.opacity));
const float scale = widget->contentScale();
if (cap.border.has_value()) {
capsuleBg->setBorder(*cap.border, Style::borderWidth * scale);
} else {
capsuleBg->clearBorder();
}
capsuleBg->setZIndex(-1);
shellPtr->addChild(std::move(capsuleBg));
auto inner = std::make_unique<Flex>();
Flex* innerPtr = inner.get();
innerPtr->setDirection(isVertical ? FlexDirection::Vertical : FlexDirection::Horizontal);
innerPtr->setGap(widgetSpacing);
innerPtr->setAlign(FlexAlign::Center);
shellPtr->addChild(std::move(inner));
BarCapsuleRun run;
run.shell = shellPtr;
run.bg = bgPtr;
run.container = innerPtr;
run.content = innerPtr;
run.spec = cap;
run.contentScale = widget->contentScale();
run.allowCircularSizing = false;
for (std::size_t memberIndex = index; memberIndex < runEnd; ++memberIndex) {
auto& member = widgets[memberIndex];
member->setBarCapsuleScene(shellPtr, bgPtr);
run.widgets.push_back(member.get());
innerPtr->addChild(member->releaseRoot());
}
capsuleRuns.push_back(std::move(run));
section->addChild(std::move(shell));
index = runEnd;
}
};
attach(instance.startWidgets, instance.startCapsuleRuns, instance.startSection);
attach(instance.centerWidgets, instance.centerCapsuleRuns, instance.centerSection);
attach(instance.endWidgets, instance.endCapsuleRuns, instance.endSection);
}
void Bar::rebuildInstanceContents(BarInstance& instance, const BarConfig& newConfig) {
// Drop any pointer hover/capture state pointing into the widgets we're about
// to destroy. Hover will be re-acquired on the next pointer motion.
instance.inputDispatcher.pointerLeave();
instance.barConfig = newConfig;
// Detach old widget root nodes from their sections and destroy the widgets.
// Widgets release their root into the section on creation, so the section
// owns those nodes — clearing the section frees the scene tree.
auto clearChildren = [](Node* node) {
if (node == nullptr) {
return;
}
while (!node->children().empty()) {
node->removeChild(node->children().back().get());
}
};
clearChildren(instance.startSection);
clearChildren(instance.centerSection);
clearChildren(instance.endSection);
instance.startWidgets.clear();
instance.centerWidgets.clear();
instance.endWidgets.clear();
instance.startCapsuleRuns.clear();
instance.centerCapsuleRuns.clear();
instance.endCapsuleRuns.clear();
// Refresh section-level layout knobs that may have changed (gap; direction
// doesn't change because position is part of the surface-fields gate).
const float widgetSpacing = static_cast<float>(instance.barConfig.widgetSpacing);
if (instance.startSection != nullptr) {
instance.startSection->setGap(widgetSpacing);
}
if (instance.centerSection != nullptr) {
instance.centerSection->setGap(widgetSpacing);
}
if (instance.endSection != nullptr) {
instance.endSection->setGap(widgetSpacing);
}
populateWidgets(instance);
attachWidgetsToSections(instance);
applyBackgroundPalette(instance);
applyBarCompositorBlur(instance);
if (instance.surface != nullptr) {
// Re-run buildScene at the current surface size so radii / styling pick
// up changes. The first-frame branch is skipped because sceneRoot is
// already in place.
const auto w = instance.surface->width();
const auto h = instance.surface->height();
if (w > 0 && h > 0) {
buildScene(instance, w, h);
}
instance.surface->requestLayout();
}
}
void Bar::tickWidgets(std::vector<std::unique_ptr<Widget>>& widgets, float deltaMs) { ::tickWidgets(widgets, deltaMs); }
bool Bar::widgetsNeedFrameTick(const std::vector<std::unique_ptr<Widget>>& widgets) {
return ::widgetsNeedFrameTick(widgets);
}
bool Bar::instanceNeedsFrameTick(const BarInstance& instance) {
return widgetsNeedFrameTick(instance.startWidgets) || widgetsNeedFrameTick(instance.centerWidgets) ||
widgetsNeedFrameTick(instance.endWidgets);
}
void Bar::applyBackgroundPalette(BarInstance& instance) {
if (instance.bg == nullptr) {
return;
}
auto style = instance.bg->style();
style.fill = colorForRole(ColorRole::Surface, instance.barConfig.backgroundOpacity);
style.border = colorForRole(ColorRole::Outline);
instance.bg->setStyle(style);
}
void Bar::syncBarSlideLayerTransform(BarInstance& instance) const {
if (instance.slideRoot == nullptr) {
return;
}
if (instance.barConfig.autoHide) {
const float t = 1.0f - instance.hideOpacity;
instance.slideRoot->setPosition(instance.slideHiddenDx * t, instance.slideHiddenDy * t);
} else {
instance.slideRoot->setPosition(0.0f, 0.0f);
}
}
void Bar::applyBarCompositorBlur(BarInstance& instance) const {
if (instance.surface == nullptr) {
return;
}
constexpr float kBlurVisibleOpacity = 0.02f;
if (instance.barConfig.autoHide && instance.hideOpacity < kBlurVisibleOpacity) {
instance.surface->clearBlurRegion();
return;
}
if (instance.bg == nullptr) {
return;
}
float absX = 0.0f;
float absY = 0.0f;
Node::absolutePosition(instance.bg, absX, absY);
const int px = static_cast<int>(std::lround(absX));
const int py = static_cast<int>(std::lround(absY));
const int pw = static_cast<int>(std::lround(std::max(0.0f, instance.bg->width())));
const int ph = static_cast<int>(std::lround(std::max(0.0f, instance.bg->height())));
auto blurStrips = Surface::tessellateRoundedRect(px, py, pw, ph, static_cast<float>(instance.barConfig.radiusTopLeft),
static_cast<float>(instance.barConfig.radiusTopRight),
static_cast<float>(instance.barConfig.radiusBottomRight),
static_cast<float>(instance.barConfig.radiusBottomLeft));
instance.surface->setBlurRegion(blurStrips);
}
void Bar::startHideFadeOut(BarInstance& instance) {
const float current = instance.hideOpacity;
instance.animations.animate(
current, 0.0f, Style::animSlow, Easing::EaseInQuad,
[this, inst = &instance](float v) {
inst->hideOpacity = v;
syncBarSlideLayerTransform(*inst);
applyBarCompositorBlur(*inst);
},
[inst = &instance]() {
if (inst->surface == nullptr) {
return;
}
const bool isVertical = (inst->barConfig.position == "left" || inst->barConfig.position == "right");
const bool isBottom = inst->barConfig.position == "bottom";
const int surfW = static_cast<int>(inst->surface->width());
const int surfH = static_cast<int>(inst->surface->height());
if (!isVertical) {
const int triggerY = isBottom ? std::max(0, surfH - kAutoHideTriggerRegionPx) : 0;
inst->surface->setInputRegion({InputRect{0, triggerY, surfW, kAutoHideTriggerRegionPx}});
} else if (inst->barConfig.position == "left") {
inst->surface->setInputRegion(
{InputRect{std::max(0, surfW - kAutoHideTriggerRegionPx), 0, kAutoHideTriggerRegionPx, surfH}});
} else {
inst->surface->setInputRegion({InputRect{0, 0, kAutoHideTriggerRegionPx, surfH}});
}
});
if (instance.surface != nullptr) {
instance.surface->requestRedraw();
}
}
void Bar::buildScene(BarInstance& instance, std::uint32_t width, std::uint32_t height) {
uiAssertNotRendering("Bar::buildScene");
if (m_renderContext == nullptr) {
return;
}
auto* renderer = m_renderContext;
const auto w = static_cast<float>(width);
const auto h = static_cast<float>(height);
const float padding = static_cast<float>(instance.barConfig.padding);
const float widgetSpacing = static_cast<float>(instance.barConfig.widgetSpacing);
const auto& shadowConfig = m_config->config().shell.shadow;
const float shadowSize = shell::surface_shadow::enabled(instance.barConfig.shadow, shadowConfig)
? static_cast<float>(shadowConfig.blur)
: 0.0f;
const float shadowOffsetX = static_cast<float>(shadowConfig.offsetX);
const float shadowOffsetY = static_cast<float>(shadowConfig.offsetY);
const bool isBottom = instance.barConfig.position == "bottom";
const bool isRight = instance.barConfig.position == "right";
const bool isVertical = (instance.barConfig.position == "left" || instance.barConfig.position == "right");
const Radii barRadii{
static_cast<float>(instance.barConfig.radiusTopLeft),
static_cast<float>(instance.barConfig.radiusTopRight),
static_cast<float>(instance.barConfig.radiusBottomRight),
static_cast<float>(instance.barConfig.radiusBottomLeft),
};
const auto barVisual = computeBarVisualGeometry(instance.barConfig, shadowConfig, w, h);
const float barAreaX = barVisual.x;
const float barAreaY = barVisual.y;
const float barAreaW = barVisual.width;
const float barAreaH = barVisual.height;
if (instance.sceneRoot == nullptr) {
instance.sceneRoot = std::make_unique<Node>();
instance.sceneRoot->setAnimationManager(&instance.animations);
instance.sceneRoot->setSize(w, h);
auto slide = std::make_unique<Node>();
slide->setParticipatesInLayout(false);
instance.slideRoot = instance.sceneRoot->addChild(std::move(slide));
// Bar background
auto bg = std::make_unique<Box>();
instance.bg = static_cast<Box*>(instance.slideRoot->addChild(std::move(bg)));
// Shadow — bar shape copy rendered with large SDF softness to simulate a blurred drop shadow.
if (shadowSize > 0.0f) {
auto shadow = std::make_unique<Box>();
instance.shadow = static_cast<Box*>(instance.slideRoot->addChild(std::move(shadow)));
auto leftClip = std::make_unique<Node>();
leftClip->setClipChildren(true);
leftClip->setZIndex(-1);
instance.shadowLeftClip = instance.slideRoot->addChild(std::move(leftClip));
auto leftShadow = std::make_unique<Box>();
instance.shadowLeft = static_cast<Box*>(instance.shadowLeftClip->addChild(std::move(leftShadow)));
auto rightClip = std::make_unique<Node>();
rightClip->setClipChildren(true);
rightClip->setZIndex(-1);
instance.shadowRightClip = instance.slideRoot->addChild(std::move(rightClip));
auto rightShadow = std::make_unique<Box>();
instance.shadowRight = static_cast<Box*>(instance.shadowRightClip->addChild(std::move(rightShadow)));
}
// Note: shadow is inserted before bar sections so it renders below them (z=-1 is set below).
auto contentClip = std::make_unique<Node>();
contentClip->setClipChildren(true);
instance.contentClip = instance.slideRoot->addChild(std::move(contentClip));
auto makeSlot = [&instance]() {
auto slot = std::make_unique<Node>();
slot->setClipChildren(true);
return instance.contentClip->addChild(std::move(slot));
};
instance.startSlot = makeSlot();
instance.centerSlot = makeSlot();
instance.endSlot = makeSlot();
// Create section boxes
auto makeSection = [widgetSpacing, isVertical]() {
auto box = std::make_unique<Flex>();
box->setDirection(isVertical ? FlexDirection::Vertical : FlexDirection::Horizontal);
box->setGap(widgetSpacing);
box->setAlign(FlexAlign::Center);
return box;
};
instance.startSection = static_cast<Flex*>(instance.startSlot->addChild(makeSection()));
instance.centerSection = static_cast<Flex*>(instance.centerSlot->addChild(makeSection()));
instance.endSection = static_cast<Flex*>(instance.endSlot->addChild(makeSection()));
attachWidgetsToSections(instance);
// Wire up InputDispatcher for this instance
instance.inputDispatcher.setSceneRoot(instance.sceneRoot.get());
instance.inputDispatcher.setCursorShapeCallback(
[this](std::uint32_t serial, std::uint32_t shape) { m_wayland->setCursorShape(serial, shape); });
if (instance.barConfig.autoHide) {
instance.slideRoot->setOpacity(1.0f);
instance.hideOpacity = 0.0f;
} else {
instance.slideRoot->setOpacity(0.0f);
instance.hideOpacity = 1.0f;
instance.animations.animate(
0.0f, 1.0f, Style::animSlow, Easing::EaseOutCubic,
[slide = instance.slideRoot](float v) { slide->setOpacity(v); }, {}, instance.slideRoot);
}
instance.surface->setSceneRoot(instance.sceneRoot.get());
}
// Update root size on reconfigure
instance.sceneRoot->setSize(w, h);
if (instance.slideRoot != nullptr) {
instance.slideRoot->setSize(w, h);
}
// Background covers only the bar visual area (not the shadow extension).
// Keep it exactly aligned with the shadow shape; the shadow shader now
// draws only outside the rect, so any size mismatch is visible at corners.
if (instance.bg != nullptr) {
const RoundedRectStyle bgStyle{
.fill = colorForRole(ColorRole::Surface, instance.barConfig.backgroundOpacity),
.border = colorForRole(ColorRole::Outline),
.fillMode = FillMode::Solid,
.radius = barRadii,
.softness = 0.0f,
.borderWidth = 0.0f,
};
instance.bg->setStyle(bgStyle);
instance.bg->setPosition(barAreaX, barAreaY);
instance.bg->setSize(barAreaW, barAreaH);
}
instance.paletteConn = paletteChanged().connect([inst = &instance] {
applyBackgroundPalette(*inst);
if (inst->surface != nullptr) {
inst->surface->requestRedraw();
}
});
if (instance.contentClip != nullptr) {
instance.contentClip->setPosition(barAreaX, barAreaY);
instance.contentClip->setSize(barAreaW, barAreaH);
}
applyBarShadowStyle(instance, shadowConfig, w, h);
layoutBarSections(instance, *renderer, barAreaW, barAreaH, padding, isVertical);
if (instance.barConfig.autoHide) {
float contentLeft = barAreaX;
float contentTop = barAreaY;
float contentRight = barAreaX + barAreaW;
float contentBottom = barAreaY + barAreaH;
if (instance.shadow != nullptr) {
const float sx = barAreaX + shadowOffsetX;
const float sy = barAreaY + shadowOffsetY;
contentLeft = std::min(contentLeft, sx);
contentTop = std::min(contentTop, sy);
contentRight = std::max(contentRight, sx + barAreaW);
contentBottom = std::max(contentBottom, sy + barAreaH);
}
const auto hiddenDelta = computeAutoHideHiddenDelta(isVertical, isBottom, isRight, w, h, contentLeft, contentTop,
contentRight, contentBottom);
instance.slideHiddenDx = hiddenDelta.first;
instance.slideHiddenDy = hiddenDelta.second;
} else {
instance.slideHiddenDx = 0.0f;
instance.slideHiddenDy = 0.0f;
}
syncBarSlideLayerTransform(instance);
const InputRect barRect{
static_cast<int>(barAreaX),
static_cast<int>(barAreaY),
static_cast<int>(barAreaW),
static_cast<int>(barAreaH),
};
if (instance.barConfig.autoHide && instance.hideOpacity < 0.5f) {
const int surfW = static_cast<int>(w);
const int surfH = static_cast<int>(h);
if (!isVertical) {
const int triggerY = isBottom ? std::max(0, surfH - kAutoHideTriggerRegionPx) : 0;
instance.surface->setInputRegion({InputRect{0, triggerY, surfW, kAutoHideTriggerRegionPx}});
} else if (instance.barConfig.position == "left") {
instance.surface->setInputRegion(
{InputRect{std::max(0, surfW - kAutoHideTriggerRegionPx), 0, kAutoHideTriggerRegionPx, surfH}});
} else {
instance.surface->setInputRegion({InputRect{0, 0, kAutoHideTriggerRegionPx, surfH}});
}
} else {
instance.surface->setInputRegion({barRect});
}
applyBarCompositorBlur(instance);
}
void Bar::updateWidgets(BarInstance& instance) {
if (m_renderContext == nullptr) {
return;
}
auto* renderer = m_renderContext;
const auto w = static_cast<float>(instance.surface->width());
const auto h = static_cast<float>(instance.surface->height());
const float padding = static_cast<float>(instance.barConfig.padding);
const float barThickness = static_cast<float>(instance.barConfig.thickness);
const float marginEnds = static_cast<float>(instance.barConfig.marginEnds);
const bool isVertical = (instance.barConfig.position == "left" || instance.barConfig.position == "right");
const auto sbi = shell::surface_shadow::bleed(instance.barConfig.shadow, m_config->config().shell.shadow);
const float bleedLeft = static_cast<float>(sbi.left);
const float bleedRight = static_cast<float>(sbi.right);
const float bleedUp = static_cast<float>(sbi.up);
const float bleedDown = static_cast<float>(sbi.down);
float barAreaW, barAreaH;
if (isVertical) {
const float barAreaY = std::min(marginEnds, bleedUp);
barAreaW = barThickness;
barAreaH = h - barAreaY - std::min(marginEnds, bleedDown);
} else {
const float barAreaX = std::min(marginEnds, bleedLeft);
barAreaW = w - barAreaX - std::min(marginEnds, bleedRight);
barAreaH = barThickness;
}
auto updateSection = [&](std::vector<std::unique_ptr<Widget>>& widgets) {
for (auto& widget : widgets) {
if (widget->root() == nullptr) {
continue;
}
widget->update(*renderer);
widget->layout(*renderer, barAreaW, barAreaH);
}
};
updateSection(instance.startWidgets);
updateSection(instance.centerWidgets);
updateSection(instance.endWidgets);
layoutBarSections(instance, *renderer, barAreaW, barAreaH, padding, isVertical);
}
void Bar::prepareFrame(BarInstance& instance, bool needsUpdate, bool needsLayout) {
if (m_renderContext == nullptr || instance.surface == nullptr) {
return;
}
m_renderContext->makeCurrent(instance.surface->renderTarget());
if (needsUpdate) {
UiPhaseScope updatePhase(UiPhase::Update);
updateWidgets(instance);
return;
}
if (!needsLayout) {
return;
}
const auto w = static_cast<float>(instance.surface->width());
const auto h = static_cast<float>(instance.surface->height());
const float padding = static_cast<float>(instance.barConfig.padding);
const float barThickness = static_cast<float>(instance.barConfig.thickness);
const float marginEnds = static_cast<float>(instance.barConfig.marginEnds);
const bool isVertical = (instance.barConfig.position == "left" || instance.barConfig.position == "right");
const auto sbi = shell::surface_shadow::bleed(instance.barConfig.shadow, m_config->config().shell.shadow);
const float bleedLeft = static_cast<float>(sbi.left);
const float bleedRight = static_cast<float>(sbi.right);
const float bleedUp = static_cast<float>(sbi.up);
const float bleedDown = static_cast<float>(sbi.down);
float barAreaW = 0.0f;
float barAreaH = 0.0f;
if (isVertical) {
const float barAreaY = std::min(marginEnds, bleedUp);
barAreaW = barThickness;
barAreaH = h - barAreaY - std::min(marginEnds, bleedDown);
} else {
const float barAreaX = std::min(marginEnds, bleedLeft);
barAreaW = w - barAreaX - std::min(marginEnds, bleedRight);
barAreaH = barThickness;
}
{
UiPhaseScope layoutPhase(UiPhase::Layout);
for (auto& widget : instance.startWidgets) {
widget->layout(*m_renderContext, barAreaW, barAreaH);
}
for (auto& widget : instance.centerWidgets) {
widget->layout(*m_renderContext, barAreaW, barAreaH);
}
for (auto& widget : instance.endWidgets) {
widget->layout(*m_renderContext, barAreaW, barAreaH);
}
layoutBarSections(instance, *m_renderContext, barAreaW, barAreaH, padding, isVertical);
}
}
bool Bar::onPointerEvent(const PointerEvent& event) {
bool consumed = false;
BarInstance* targetInstance = nullptr;
if (event.surface != nullptr) {
targetInstance = instanceForSurface(event.surface);
} else {
targetInstance = m_hoveredInstance;
}
auto routeWidgetPopups = [&](BarInstance& instance) {
auto routeGroup = [&](std::vector<std::unique_ptr<Widget>>& widgets) {
for (auto& widget : widgets) {
if (widget != nullptr && widget->onPointerEvent(event)) {
return true;
}
}
return false;
};
return routeGroup(instance.startWidgets) || routeGroup(instance.centerWidgets) || routeGroup(instance.endWidgets);
};
if (targetInstance != nullptr) {
if (routeWidgetPopups(*targetInstance)) {
return true;
}
} else {
for (const auto& instance : m_instances) {
if (instance != nullptr && routeWidgetPopups(*instance)) {
return true;
}
}
}
if (targetInstance != nullptr && targetInstance->attachedPopupCount > 0) {
switch (event.type) {
case PointerEvent::Type::Enter:
m_hoveredInstance = targetInstance;
targetInstance->pointerInside = true;
break;
case PointerEvent::Type::Leave:
targetInstance->pointerInside = false;
if (m_hoveredInstance == targetInstance) {
m_hoveredInstance = nullptr;
}
break;
case PointerEvent::Type::Motion:
case PointerEvent::Type::Button:
case PointerEvent::Type::Axis:
if (event.type == PointerEvent::Type::Button && event.button == BTN_RIGHT && event.state == 1) {
auto& panelManager = PanelManager::instance();
if (panelManager.isOpenPanel("control-center")) {
panelManager.closePanel();
} else {
panelManager.openPanel("control-center", PanelOpenRequest{.output = targetInstance->output,
.context = "overview",
.sourceBarName = targetInstance->barConfig.name});
}
return true;
}
break;
}
return false;
}
switch (event.type) {
case PointerEvent::Type::Enter: {
auto it = m_surfaceMap.find(event.surface);
if (it == m_surfaceMap.end()) {
break;
}
m_hoveredInstance = it->second;
m_hoveredInstance->lastPointerSx = static_cast<float>(event.sx);
m_hoveredInstance->lastPointerSy = static_cast<float>(event.sy);
m_hoveredInstance->pointerInside = true;
m_hoveredInstance->inputDispatcher.pointerEnter(static_cast<float>(event.sx), static_cast<float>(event.sy),
event.serial);
if (m_hoveredInstance->barConfig.autoHide && m_hoveredInstance->sceneRoot != nullptr) {
const float current = m_hoveredInstance->hideOpacity;
m_hoveredInstance->animations.animate(current, 1.0f, Style::animNormal, Easing::EaseOutCubic,
[inst = m_hoveredInstance, this](float v) {
inst->hideOpacity = v;
syncBarSlideLayerTransform(*inst);
applyBarCompositorBlur(*inst);
});
if (m_hoveredInstance->surface != nullptr) {
const int sw = static_cast<int>(m_hoveredInstance->surface->width());
const int sh = static_cast<int>(m_hoveredInstance->surface->height());
m_hoveredInstance->surface->setInputRegion({InputRect{0, 0, sw, sh}});
}
m_hoveredInstance->surface->requestRedraw();
}
break;
}
case PointerEvent::Type::Leave: {
if (m_hoveredInstance != nullptr) {
m_hoveredInstance->pointerInside = false;
m_hoveredInstance->inputDispatcher.pointerLeave();
const bool suppressAutoHide =
(m_autoHideSuppressionCallback != nullptr) ? m_autoHideSuppressionCallback() : false;
if (m_hoveredInstance->barConfig.autoHide && !suppressAutoHide) {
startHideFadeOut(*m_hoveredInstance);
}
m_hoveredInstance = nullptr;
}
break;
}
case PointerEvent::Type::Motion: {
if (m_hoveredInstance == nullptr)
break;
m_hoveredInstance->lastPointerSx = static_cast<float>(event.sx);
m_hoveredInstance->lastPointerSy = static_cast<float>(event.sy);
m_hoveredInstance->inputDispatcher.pointerMotion(static_cast<float>(event.sx), static_cast<float>(event.sy), 0);
break;
}
case PointerEvent::Type::Button: {
if (m_hoveredInstance == nullptr)
break;
m_hoveredInstance->lastPointerSx = static_cast<float>(event.sx);
m_hoveredInstance->lastPointerSy = static_cast<float>(event.sy);
bool pressed = (event.state == 1); // WL_POINTER_BUTTON_STATE_PRESSED
consumed = m_hoveredInstance->inputDispatcher.pointerButton(static_cast<float>(event.sx),
static_cast<float>(event.sy), event.button, pressed);
if (pressed && event.button == BTN_RIGHT && !consumed) {
const float sx = static_cast<float>(event.sx);
const float sy = static_cast<float>(event.sy);
const bool insideAnySection = pointInsideNode(m_hoveredInstance->startSection, sx, sy) ||
pointInsideNode(m_hoveredInstance->centerSection, sx, sy) ||
pointInsideNode(m_hoveredInstance->endSection, sx, sy);
if (!insideAnySection) {
auto& panelManager = PanelManager::instance();
if (panelManager.isOpenPanel("control-center")) {
panelManager.closePanel();
} else {
panelManager.openPanel("control-center",
PanelOpenRequest{.output = m_hoveredInstance->output,
.context = "overview",
.sourceBarName = m_hoveredInstance->barConfig.name});
}
consumed = true;
}
}
break;
}
case PointerEvent::Type::Axis: {
if (m_hoveredInstance == nullptr)
break;
m_hoveredInstance->lastPointerSx = static_cast<float>(event.sx);
m_hoveredInstance->lastPointerSy = static_cast<float>(event.sy);
m_hoveredInstance->inputDispatcher.pointerAxis(static_cast<float>(event.sx), static_cast<float>(event.sy),
event.axis, event.axisSource, event.axisValue, event.axisDiscrete,
event.axisValue120, event.axisLines);
break;
}
}
// Trigger redraw if any widget changed visual state
if (m_hoveredInstance != nullptr && m_hoveredInstance->sceneRoot != nullptr &&
(m_hoveredInstance->sceneRoot->paintDirty() || m_hoveredInstance->sceneRoot->layoutDirty())) {
if (m_hoveredInstance->sceneRoot->layoutDirty()) {
m_hoveredInstance->surface->requestLayout();
} else {
m_hoveredInstance->surface->requestRedraw();
}
}
return consumed;
}
BarInstance* Bar::instanceForSurface(wl_surface* surface) const noexcept {
if (surface == nullptr) {
return nullptr;
}
const auto it = m_surfaceMap.find(surface);
return it != m_surfaceMap.end() ? it->second : nullptr;
}
BarInstance* Bar::instanceForOutput(wl_output* output) const noexcept {
if (output == nullptr) {
return nullptr;
}
for (const auto& instance : m_instances) {
if (instance != nullptr && instance->output == output && instance->surface != nullptr) {
return instance.get();
}
}
return nullptr;
}
void Bar::registerIpc(IpcService& ipc) {
ipc.registerHandler(
"bar-show",
[this](const std::string&) -> std::string {
show();
return "ok\n";
},
"bar-show", "Show the bar");
ipc.registerHandler(
"bar-hide",
[this](const std::string&) -> std::string {
hide();
return "ok\n";
},
"bar-hide", "Hide the bar");
ipc.registerHandler(
"bar-toggle",
[this](const std::string&) -> std::string {
isVisible() ? hide() : show();
return "ok\n";
},
"bar-toggle", "Toggle bar visibility");
}