mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(osd): add reusable osd overlay with audio support
This commit is contained in:
@@ -241,6 +241,8 @@ add_executable(noctalia
|
||||
src/render/scene/input_area.cpp
|
||||
src/render/scene/input_dispatcher.cpp
|
||||
src/shell/notification/notification_popup.cpp
|
||||
src/shell/osd/audio_osd.cpp
|
||||
src/shell/osd/osd_overlay.cpp
|
||||
src/shell/tray/tray_menu.cpp
|
||||
src/shell/panel/panel_manager.cpp
|
||||
src/shell/panels/notification_history_panel.cpp
|
||||
|
||||
@@ -12,6 +12,7 @@ Changes are detected automatically via inotify — no restart required.
|
||||
- [Per-monitor overrides](#per-monitor-overrides)
|
||||
- [Widget definitions](#widget-definitions)
|
||||
- [Built-in widgets](#built-in-widgets)
|
||||
- [OSD](#osd)
|
||||
- [Wallpaper](#wallpaper)
|
||||
- [Full example](#full-example)
|
||||
|
||||
@@ -220,6 +221,17 @@ edge_smoothness = 0.5 # 0.0 – 1.0
|
||||
|
||||
---
|
||||
|
||||
## OSD
|
||||
|
||||
```toml
|
||||
[osd]
|
||||
position = "top_right" # top_right | top_left | top_center | bottom_right | bottom_left | bottom_center
|
||||
```
|
||||
|
||||
The OSD currently powers the volume HUD and defaults to `top_right`.
|
||||
|
||||
---
|
||||
|
||||
## Full example
|
||||
|
||||
```toml
|
||||
@@ -258,6 +270,9 @@ match = "DP-1" # main 4K display — taller bar, show seconds
|
||||
height = 44
|
||||
end = ["tray", "notifications", "volume", "battery", "clock-seconds"]
|
||||
|
||||
[osd]
|
||||
position = "top_right"
|
||||
|
||||
[bar.main.monitor.hdmi]
|
||||
match = "HDMI-A-1" # secondary 1080p — smaller, minimal widgets
|
||||
height = 34
|
||||
|
||||
@@ -210,7 +210,7 @@ gdbus call --session --dest dev.noctalia.Debug --object-path /dev/noctalia/Debug
|
||||
|
||||
### Controls (`src/ui/controls/`)
|
||||
|
||||
- [ ] Scroll view
|
||||
- [x] Scroll view
|
||||
- [ ] List view
|
||||
- [ ] Checkbox
|
||||
- [ ] Radio button
|
||||
|
||||
+19
-1
@@ -5,6 +5,8 @@
|
||||
#include "shell/panels/notification_history_panel.h"
|
||||
#include "shell/panels/test_panel.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <csignal>
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -142,7 +144,6 @@ void Application::run() {
|
||||
|
||||
try {
|
||||
m_pipewireService = std::make_unique<PipeWireService>();
|
||||
m_pipewireService->setChangeCallback([this]() { m_bar.onWorkspaceChange(); });
|
||||
const auto* sink = m_pipewireService->defaultSink();
|
||||
if (sink != nullptr) {
|
||||
logInfo("pipewire: default sink \"{}\" vol={:.0f}%", sink->description, sink->volume * 100.0f);
|
||||
@@ -214,6 +215,13 @@ void Application::run() {
|
||||
// Initialize notification popup (top layer, dynamic surface)
|
||||
m_notificationPopup.initialize(m_wayland, &m_configService, &m_notificationManager, &m_renderContext);
|
||||
|
||||
// Initialize the shared OSD overlay (overlay layer, transient surface)
|
||||
m_osdOverlay.initialize(m_wayland, &m_configService, &m_renderContext);
|
||||
m_audioOsd.bindOverlay(m_osdOverlay);
|
||||
if (m_pipewireService != nullptr) {
|
||||
m_audioOsd.primeFromService(*m_pipewireService);
|
||||
}
|
||||
|
||||
// Initialize tray menu (top layer, dynamic surface)
|
||||
m_trayMenu.initialize(m_wayland, &m_configService, m_trayService.get(), &m_renderContext);
|
||||
|
||||
@@ -221,6 +229,16 @@ void Application::run() {
|
||||
m_bar.initialize(m_wayland, &m_configService, &m_timeService, &m_notificationManager, m_trayService.get(),
|
||||
m_pipewireService.get(), m_upowerService.get(), m_systemMonitor.get(), &m_renderContext);
|
||||
|
||||
if (m_pipewireService != nullptr) {
|
||||
m_audioOsd.suppressFor(std::chrono::milliseconds(2000));
|
||||
m_pipewireService->setChangeCallback([this]() {
|
||||
m_bar.onWorkspaceChange();
|
||||
if (m_pipewireService != nullptr) {
|
||||
m_audioOsd.onAudioStateChanged(*m_pipewireService);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Unified pointer event routing — both Bar and PanelManager check surface ownership
|
||||
m_wayland.setPointerEventCallback([this](const PointerEvent& event) {
|
||||
if (m_trayMenu.onPointerEvent(event))
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "render/render_context.h"
|
||||
#include "shell/bar/bar.h"
|
||||
#include "shell/notification/notification_popup.h"
|
||||
#include "shell/osd/audio_osd.h"
|
||||
#include "shell/osd/osd_overlay.h"
|
||||
#include "shell/panel/panel_manager.h"
|
||||
#include "shell/tray/tray_menu.h"
|
||||
#include "shell/wallpaper/wallpaper.h"
|
||||
@@ -65,6 +67,8 @@ private:
|
||||
Bar m_bar;
|
||||
PanelManager m_panelManager;
|
||||
NotificationPopup m_notificationPopup;
|
||||
AudioOsd m_audioOsd;
|
||||
OsdOverlay m_osdOverlay;
|
||||
TrayMenu m_trayMenu;
|
||||
Wallpaper m_wallpaper;
|
||||
|
||||
|
||||
@@ -379,6 +379,13 @@ void ConfigService::loadFromFile(const std::string& path) {
|
||||
wp.edgeSmoothness = static_cast<float>(*v);
|
||||
}
|
||||
|
||||
// Parse [osd]
|
||||
if (auto* osdTbl = tbl["osd"].as_table()) {
|
||||
auto& osd = m_config.osd;
|
||||
if (auto v = (*osdTbl)["position"].value<std::string>())
|
||||
osd.position = *v;
|
||||
}
|
||||
|
||||
if (m_config.bars.empty()) {
|
||||
logInfo("config: no [bar.*] defined, using defaults");
|
||||
m_config.bars.push_back(BarConfig{});
|
||||
|
||||
@@ -82,10 +82,15 @@ struct WallpaperConfig {
|
||||
float edgeSmoothness = 0.5f;
|
||||
};
|
||||
|
||||
struct OsdConfig {
|
||||
std::string position = "top_right";
|
||||
};
|
||||
|
||||
struct Config {
|
||||
std::vector<BarConfig> bars;
|
||||
std::unordered_map<std::string, WidgetConfig> widgets;
|
||||
WallpaperConfig wallpaper;
|
||||
OsdConfig osd;
|
||||
};
|
||||
|
||||
class ConfigService {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#include "shell/osd/audio_osd.h"
|
||||
|
||||
#include "pipewire/pipewire_service.h"
|
||||
#include "shell/osd/osd_overlay.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
const char* volumeIconName(float volume, bool muted) {
|
||||
if (muted || volume <= 0.0f) {
|
||||
return "volume-mute";
|
||||
}
|
||||
if (volume < 0.4f) {
|
||||
return "volume-low";
|
||||
}
|
||||
return "volume-high";
|
||||
}
|
||||
|
||||
OsdContent makeOutputContent(float volume, bool muted) {
|
||||
const int percent = static_cast<int>(std::round(std::max(0.0f, volume) * 100.0f));
|
||||
return OsdContent{
|
||||
.icon = volumeIconName(volume, muted),
|
||||
.value = std::to_string(percent) + "%",
|
||||
.progress = std::clamp(volume, 0.0f, 1.0f),
|
||||
};
|
||||
}
|
||||
|
||||
OsdContent makeInputContent(float volume, bool muted) {
|
||||
const int percent = static_cast<int>(std::round(std::max(0.0f, volume) * 100.0f));
|
||||
return OsdContent{
|
||||
.icon = muted ? "microphone-mute" : "microphone",
|
||||
.value = std::to_string(percent) + "%",
|
||||
.progress = std::clamp(volume, 0.0f, 1.0f),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AudioOsd::bindOverlay(OsdOverlay& overlay) { m_overlay = &overlay; }
|
||||
|
||||
void AudioOsd::primeFromService(const PipeWireService& service) {
|
||||
if (const auto* sink = service.defaultSink(); sink != nullptr) {
|
||||
m_lastSinkId = sink->id;
|
||||
m_lastSinkPercent = static_cast<int>(std::round(std::max(0.0f, sink->volume) * 100.0f));
|
||||
m_lastSinkMuted = sink->muted;
|
||||
}
|
||||
|
||||
if (const auto* source = service.defaultSource(); source != nullptr) {
|
||||
m_lastSourceId = source->id;
|
||||
m_lastSourcePercent = static_cast<int>(std::round(std::max(0.0f, source->volume) * 100.0f));
|
||||
m_lastSourceMuted = source->muted;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioOsd::suppressFor(std::chrono::milliseconds duration) {
|
||||
m_suppressUntil = std::chrono::steady_clock::now() + duration;
|
||||
}
|
||||
|
||||
void AudioOsd::onAudioStateChanged(const PipeWireService& service) {
|
||||
const auto* sink = service.defaultSink();
|
||||
const auto* source = service.defaultSource();
|
||||
|
||||
const std::uint32_t sinkId = sink != nullptr ? sink->id : 0;
|
||||
const int sinkPercent =
|
||||
sink != nullptr ? static_cast<int>(std::round(std::max(0.0f, sink->volume) * 100.0f)) : 0;
|
||||
const bool sinkMuted = sink != nullptr ? sink->muted : false;
|
||||
|
||||
const std::uint32_t sourceId = source != nullptr ? source->id : 0;
|
||||
const int sourcePercent =
|
||||
source != nullptr ? static_cast<int>(std::round(std::max(0.0f, source->volume) * 100.0f)) : 0;
|
||||
const bool sourceMuted = source != nullptr ? source->muted : false;
|
||||
|
||||
if (std::chrono::steady_clock::now() < m_suppressUntil) {
|
||||
m_lastSinkId = sinkId;
|
||||
m_lastSinkPercent = sinkPercent;
|
||||
m_lastSinkMuted = sinkMuted;
|
||||
m_lastSourceId = sourceId;
|
||||
m_lastSourcePercent = sourcePercent;
|
||||
m_lastSourceMuted = sourceMuted;
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_overlay != nullptr) {
|
||||
if (sink != nullptr &&
|
||||
(sinkId != m_lastSinkId || sinkPercent != m_lastSinkPercent || sinkMuted != m_lastSinkMuted)) {
|
||||
m_overlay->show(makeOutputContent(sink->volume, sinkMuted));
|
||||
} else if (source != nullptr && (sourceId != m_lastSourceId || sourcePercent != m_lastSourcePercent ||
|
||||
sourceMuted != m_lastSourceMuted)) {
|
||||
m_overlay->show(makeInputContent(source->volume, sourceMuted));
|
||||
}
|
||||
}
|
||||
|
||||
m_lastSinkId = sinkId;
|
||||
m_lastSinkPercent = sinkPercent;
|
||||
m_lastSinkMuted = sinkMuted;
|
||||
m_lastSourceId = sourceId;
|
||||
m_lastSourcePercent = sourcePercent;
|
||||
m_lastSourceMuted = sourceMuted;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
class OsdOverlay;
|
||||
class PipeWireService;
|
||||
|
||||
class AudioOsd {
|
||||
public:
|
||||
void bindOverlay(OsdOverlay& overlay);
|
||||
void primeFromService(const PipeWireService& service);
|
||||
void suppressFor(std::chrono::milliseconds duration);
|
||||
void onAudioStateChanged(const PipeWireService& service);
|
||||
|
||||
private:
|
||||
OsdOverlay* m_overlay = nullptr;
|
||||
std::uint32_t m_lastSinkId = 0;
|
||||
int m_lastSinkPercent = -1;
|
||||
bool m_lastSinkMuted = false;
|
||||
std::uint32_t m_lastSourceId = 0;
|
||||
int m_lastSourcePercent = -1;
|
||||
bool m_lastSourceMuted = false;
|
||||
std::chrono::steady_clock::time_point m_suppressUntil{};
|
||||
};
|
||||
@@ -0,0 +1,319 @@
|
||||
#include "shell/osd/osd_overlay.h"
|
||||
|
||||
#include "config/config_service.h"
|
||||
#include "core/log.h"
|
||||
#include "render/render_context.h"
|
||||
#include "render/scene/node.h"
|
||||
#include "ui/controls/box.h"
|
||||
#include "ui/controls/icon.h"
|
||||
#include "ui/controls/label.h"
|
||||
#include "ui/palette.h"
|
||||
#include "ui/style.h"
|
||||
#include "wayland/wayland_connection.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kSurfaceWidth = 360;
|
||||
constexpr int kSurfaceHeight = 76;
|
||||
constexpr int kCardWidth = 348;
|
||||
constexpr int kCardHeight = 52;
|
||||
constexpr int kHideDelayMs = 1400;
|
||||
|
||||
constexpr float kCardOpacity = 0.98f;
|
||||
constexpr float kIconSize = 20.0f;
|
||||
constexpr float kValueFontSize = static_cast<float>(Style::fontSizeTitle);
|
||||
|
||||
constexpr float kProgressHeight = 8.0f;
|
||||
constexpr float kCardPadding = static_cast<float>(Style::spaceMd);
|
||||
constexpr float kInnerGap = static_cast<float>(Style::spaceMd);
|
||||
constexpr int kScreenMargin = Style::spaceLg;
|
||||
constexpr int kBarGap = 4;
|
||||
|
||||
} // namespace
|
||||
|
||||
void OsdOverlay::initialize(WaylandConnection& wayland, ConfigService* config, RenderContext* renderContext) {
|
||||
m_wayland = &wayland;
|
||||
m_config = config;
|
||||
m_renderContext = renderContext;
|
||||
}
|
||||
|
||||
void OsdOverlay::show(const OsdContent& content) {
|
||||
if (m_wayland == nullptr || m_renderContext == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_content = content;
|
||||
ensureSurfaces();
|
||||
for (auto& inst : m_instances) {
|
||||
if (inst->sceneRoot == nullptr) {
|
||||
continue;
|
||||
}
|
||||
updateInstanceContent(*inst);
|
||||
animateInstance(*inst);
|
||||
inst->surface->requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
void OsdOverlay::ensureSurfaces() {
|
||||
if (m_wayland == nullptr || m_renderContext == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string position =
|
||||
(m_config != nullptr && !m_config->config().osd.position.empty()) ? m_config->config().osd.position : "top_right";
|
||||
if (!m_instances.empty() && position != m_lastPosition) {
|
||||
destroySurfaces();
|
||||
}
|
||||
|
||||
if (!m_instances.empty() && m_instances.size() != m_wayland->outputs().size()) {
|
||||
destroySurfaces();
|
||||
}
|
||||
if (!m_instances.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_lastPosition = position;
|
||||
|
||||
const auto surfaceWidth = static_cast<std::uint32_t>(kSurfaceWidth);
|
||||
const auto surfaceHeight = static_cast<std::uint32_t>(kSurfaceHeight);
|
||||
std::int32_t barHeight = Style::barHeightDefault;
|
||||
std::string barPosition = "top";
|
||||
if (m_config != nullptr && !m_config->config().bars.empty()) {
|
||||
barHeight = m_config->config().bars[0].height;
|
||||
barPosition = m_config->config().bars[0].position;
|
||||
}
|
||||
|
||||
for (const auto& output : m_wayland->outputs()) {
|
||||
auto inst = std::make_unique<Instance>();
|
||||
inst->output = output.output;
|
||||
inst->scale = output.scale;
|
||||
|
||||
std::uint32_t anchor = LayerShellAnchor::Top | LayerShellAnchor::Right;
|
||||
std::int32_t marginTop = kScreenMargin;
|
||||
std::int32_t marginRight = kScreenMargin;
|
||||
std::int32_t marginBottom = 0;
|
||||
std::int32_t marginLeft = 0;
|
||||
|
||||
if (position == "top_left") {
|
||||
anchor = LayerShellAnchor::Top | LayerShellAnchor::Left;
|
||||
marginRight = 0;
|
||||
marginLeft = kScreenMargin;
|
||||
} else if (position == "top_center") {
|
||||
anchor = LayerShellAnchor::Top;
|
||||
marginRight = 0;
|
||||
} else if (position == "bottom_left") {
|
||||
anchor = LayerShellAnchor::Bottom | LayerShellAnchor::Left;
|
||||
marginTop = 0;
|
||||
marginRight = 0;
|
||||
marginBottom = kScreenMargin;
|
||||
marginLeft = kScreenMargin;
|
||||
} else if (position == "bottom_center") {
|
||||
anchor = LayerShellAnchor::Bottom;
|
||||
marginTop = 0;
|
||||
marginRight = 0;
|
||||
marginBottom = kScreenMargin;
|
||||
} else if (position == "bottom_right") {
|
||||
anchor = LayerShellAnchor::Bottom | LayerShellAnchor::Right;
|
||||
marginTop = 0;
|
||||
marginBottom = kScreenMargin;
|
||||
}
|
||||
|
||||
if ((position == "top_left" || position == "top_center" || position == "top_right") && barPosition == "top") {
|
||||
marginTop += barHeight + kBarGap;
|
||||
}
|
||||
if ((position == "bottom_left" || position == "bottom_center" || position == "bottom_right") &&
|
||||
barPosition == "bottom") {
|
||||
marginBottom += barHeight + kBarGap;
|
||||
}
|
||||
|
||||
auto surfaceConfig = LayerSurfaceConfig{
|
||||
.nameSpace = "noctalia-osd",
|
||||
.layer = LayerShellLayer::Overlay,
|
||||
.anchor = anchor,
|
||||
.width = surfaceWidth,
|
||||
.height = surfaceHeight,
|
||||
.exclusiveZone = -1,
|
||||
.marginTop = marginTop,
|
||||
.marginRight = marginRight,
|
||||
.marginBottom = marginBottom,
|
||||
.marginLeft = marginLeft,
|
||||
.keyboard = LayerShellKeyboard::None,
|
||||
.defaultWidth = surfaceWidth,
|
||||
.defaultHeight = surfaceHeight,
|
||||
};
|
||||
|
||||
inst->surface = std::make_unique<LayerSurface>(*m_wayland, std::move(surfaceConfig));
|
||||
auto* instPtr = inst.get();
|
||||
inst->surface->setConfigureCallback([this, instPtr](std::uint32_t width, std::uint32_t height) {
|
||||
buildScene(*instPtr, width, height);
|
||||
});
|
||||
inst->surface->setAnimationManager(&inst->animations);
|
||||
inst->surface->setRenderContext(m_renderContext);
|
||||
|
||||
if (!inst->surface->initialize(output.output, output.scale)) {
|
||||
logWarn("osd overlay: failed to initialize surface on {}", output.connectorName);
|
||||
continue;
|
||||
}
|
||||
|
||||
inst->wlSurface = inst->surface->wlSurface();
|
||||
m_instances.push_back(std::move(inst));
|
||||
}
|
||||
}
|
||||
|
||||
void OsdOverlay::destroySurfaces() {
|
||||
for (auto& inst : m_instances) {
|
||||
inst->animations.cancelAll();
|
||||
}
|
||||
m_instances.clear();
|
||||
}
|
||||
|
||||
void OsdOverlay::buildScene(Instance& inst, std::uint32_t width, std::uint32_t height) {
|
||||
if (m_renderContext == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float w = static_cast<float>(width);
|
||||
const float h = static_cast<float>(height);
|
||||
|
||||
inst.sceneRoot = std::make_unique<Node>();
|
||||
inst.sceneRoot->setSize(w, h);
|
||||
inst.sceneRoot->setOpacity(0.0f);
|
||||
inst.surface->setSceneRoot(inst.sceneRoot.get());
|
||||
|
||||
const float cardX = (w - static_cast<float>(kCardWidth)) * 0.5f;
|
||||
const float cardY = (h - static_cast<float>(kCardHeight)) * 0.5f;
|
||||
|
||||
auto background = std::make_unique<Box>();
|
||||
background->setCardStyle();
|
||||
background->setFill(palette.surface);
|
||||
background->setBorder(palette.outline, static_cast<float>(Style::borderWidth));
|
||||
background->setRadius(static_cast<float>(kCardHeight) * 0.5f);
|
||||
background->setSoftness(1.2f);
|
||||
background->setSize(static_cast<float>(kCardWidth), static_cast<float>(kCardHeight));
|
||||
background->setPosition(cardX, cardY);
|
||||
background->setZIndex(0);
|
||||
inst.background = background.get();
|
||||
inst.sceneRoot->addChild(std::move(background));
|
||||
|
||||
auto card = std::make_unique<Node>();
|
||||
card->setSize(static_cast<float>(kCardWidth), static_cast<float>(kCardHeight));
|
||||
card->setPosition(cardX, cardY);
|
||||
card->setZIndex(1);
|
||||
inst.card = card.get();
|
||||
|
||||
auto icon = std::make_unique<Icon>();
|
||||
icon->setIconSize(kIconSize);
|
||||
icon->setColor(palette.primary);
|
||||
inst.icon = icon.get();
|
||||
inst.icon->setZIndex(1);
|
||||
inst.card->addChild(std::move(icon));
|
||||
|
||||
auto value = std::make_unique<Label>();
|
||||
value->setBold(true);
|
||||
value->setFontSize(kValueFontSize);
|
||||
value->setColor(palette.onSurface);
|
||||
inst.value = value.get();
|
||||
inst.value->setZIndex(1);
|
||||
inst.card->addChild(std::move(value));
|
||||
|
||||
auto progress = std::make_unique<ProgressBar>();
|
||||
progress->setRadius(static_cast<float>(Style::radiusFull));
|
||||
progress->setTrackColor(palette.surface);
|
||||
progress->setFillColor(palette.primary);
|
||||
inst.progress = progress.get();
|
||||
inst.progress->setZIndex(1);
|
||||
inst.card->addChild(std::move(progress));
|
||||
|
||||
inst.sceneRoot->addChild(std::move(card));
|
||||
updateInstanceContent(inst);
|
||||
}
|
||||
|
||||
void OsdOverlay::updateInstanceContent(Instance& inst) {
|
||||
if (m_renderContext == nullptr || inst.card == nullptr || inst.icon == nullptr || inst.value == nullptr ||
|
||||
inst.progress == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float cardWidth = inst.card->width();
|
||||
inst.icon->setIcon(m_content.icon);
|
||||
inst.icon->measure(*m_renderContext);
|
||||
inst.icon->setPosition(kCardPadding,
|
||||
std::round((inst.card->height() - inst.icon->height()) * 0.5f) - 1.0f);
|
||||
|
||||
inst.value->setText(m_content.value);
|
||||
inst.value->setMaxWidth(cardWidth);
|
||||
inst.value->measure(*m_renderContext);
|
||||
inst.value->setPosition(cardWidth - kCardPadding - inst.value->width(),
|
||||
std::round((inst.card->height() - inst.value->height()) * 0.5f));
|
||||
|
||||
const float progressX = inst.icon->x() + inst.icon->width() + kInnerGap;
|
||||
const float progressWidth = std::max(0.0f, inst.value->x() - progressX - kInnerGap);
|
||||
inst.progress->setSize(progressWidth, kProgressHeight);
|
||||
inst.progress->setRadius(kProgressHeight * 0.5f);
|
||||
inst.progress->setPosition(progressX, std::round((inst.card->height() - kProgressHeight) * 0.5f));
|
||||
inst.progress->setProgress(m_content.progress);
|
||||
}
|
||||
|
||||
void OsdOverlay::animateInstance(Instance& inst) {
|
||||
if (inst.sceneRoot == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inst.showAnimId != 0) {
|
||||
inst.animations.cancel(inst.showAnimId);
|
||||
inst.showAnimId = 0;
|
||||
}
|
||||
if (inst.hideAnimId != 0) {
|
||||
inst.animations.cancel(inst.hideAnimId);
|
||||
inst.hideAnimId = 0;
|
||||
}
|
||||
|
||||
const float baseY = (inst.sceneRoot->height() - static_cast<float>(kCardHeight)) * 0.5f;
|
||||
if (!inst.visible) {
|
||||
inst.sceneRoot->setOpacity(0.0f);
|
||||
inst.card->setPosition(inst.card->x(), baseY + 8.0f);
|
||||
if (inst.background != nullptr) {
|
||||
inst.background->setPosition(inst.background->x(), baseY + 8.0f);
|
||||
}
|
||||
inst.showAnimId = inst.animations.animate(
|
||||
0.0f, 1.0f, Style::animNormal, Easing::EaseOutCubic,
|
||||
[&inst, baseY](float v) {
|
||||
inst.sceneRoot->setOpacity(v);
|
||||
inst.card->setPosition(inst.card->x(), baseY + (1.0f - v) * 8.0f);
|
||||
if (inst.background != nullptr) {
|
||||
inst.background->setPosition(inst.background->x(), baseY + (1.0f - v) * 8.0f);
|
||||
}
|
||||
},
|
||||
[&inst]() {
|
||||
inst.showAnimId = 0;
|
||||
inst.visible = true;
|
||||
});
|
||||
} else {
|
||||
inst.sceneRoot->setOpacity(1.0f);
|
||||
inst.card->setPosition(inst.card->x(), baseY);
|
||||
if (inst.background != nullptr) {
|
||||
inst.background->setPosition(inst.background->x(), baseY);
|
||||
}
|
||||
}
|
||||
|
||||
inst.hideAnimId = inst.animations.animate(
|
||||
1.0f, 0.0f, kHideDelayMs, Easing::Linear, [](float /*v*/) {},
|
||||
[&inst, baseY]() {
|
||||
inst.hideAnimId = inst.animations.animate(
|
||||
1.0f, 0.0f, Style::animNormal, Easing::EaseInOutQuad,
|
||||
[&inst, baseY](float v) {
|
||||
inst.sceneRoot->setOpacity(v);
|
||||
inst.card->setPosition(inst.card->x(), baseY + (1.0f - v) * 6.0f);
|
||||
if (inst.background != nullptr) {
|
||||
inst.background->setPosition(inst.background->x(), baseY + (1.0f - v) * 6.0f);
|
||||
}
|
||||
},
|
||||
[&inst]() {
|
||||
inst.hideAnimId = 0;
|
||||
inst.visible = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "render/animation/animation_manager.h"
|
||||
#include "ui/controls/progress_bar.h"
|
||||
#include "wayland/layer_surface.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class ConfigService;
|
||||
class Box;
|
||||
class Icon;
|
||||
class Label;
|
||||
class Node;
|
||||
class RenderContext;
|
||||
class WaylandConnection;
|
||||
struct wl_surface;
|
||||
|
||||
struct OsdContent {
|
||||
std::string icon;
|
||||
std::string value;
|
||||
float progress = 0.0f;
|
||||
};
|
||||
|
||||
class OsdOverlay {
|
||||
public:
|
||||
OsdOverlay() = default;
|
||||
~OsdOverlay() = default;
|
||||
|
||||
OsdOverlay(const OsdOverlay&) = delete;
|
||||
OsdOverlay& operator=(const OsdOverlay&) = delete;
|
||||
|
||||
void initialize(WaylandConnection& wayland, ConfigService* config, RenderContext* renderContext);
|
||||
|
||||
void show(const OsdContent& content);
|
||||
|
||||
private:
|
||||
struct Instance {
|
||||
wl_output* output = nullptr;
|
||||
std::int32_t scale = 1;
|
||||
std::unique_ptr<LayerSurface> surface;
|
||||
std::unique_ptr<Node> sceneRoot;
|
||||
AnimationManager animations;
|
||||
wl_surface* wlSurface = nullptr;
|
||||
|
||||
Node* card = nullptr;
|
||||
Box* background = nullptr;
|
||||
Icon* icon = nullptr;
|
||||
Label* value = nullptr;
|
||||
ProgressBar* progress = nullptr;
|
||||
AnimationManager::Id showAnimId = 0;
|
||||
AnimationManager::Id hideAnimId = 0;
|
||||
bool visible = false;
|
||||
};
|
||||
|
||||
void ensureSurfaces();
|
||||
void destroySurfaces();
|
||||
void buildScene(Instance& inst, std::uint32_t width, std::uint32_t height);
|
||||
void updateInstanceContent(Instance& inst);
|
||||
void animateInstance(Instance& inst);
|
||||
|
||||
WaylandConnection* m_wayland = nullptr;
|
||||
ConfigService* m_config = nullptr;
|
||||
RenderContext* m_renderContext = nullptr;
|
||||
OsdContent m_content;
|
||||
std::string m_lastPosition;
|
||||
std::vector<std::unique_ptr<Instance>> m_instances;
|
||||
};
|
||||
Reference in New Issue
Block a user