chore(clangd): new clang-format rules + actually applies to all cpp/h files

This commit is contained in:
Lemmy
2026-04-18 10:21:11 -04:00
parent ef294d4886
commit f11eee4522
185 changed files with 4864 additions and 5025 deletions
+7
View File
@@ -3,3 +3,10 @@ IndentWidth: 2
ColumnLimit: 120
PointerAlignment: Left
NamespaceIndentation: All
SortIncludes: CaseSensitive
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^".*"'
Priority: 1
- Regex: '^<.*>'
Priority: 2
+2 -2
View File
@@ -25,8 +25,8 @@ run m=mode:
./build-{{m}}/noctalia
format:
clang-format -i src/**/*.cpp src/**/*.h
find src \( -name '*.cpp' -o -name '*.h' \) | xargs grep -rlP '\s+$' | xargs -r sed -i 's/[[:space:]]*$//'
find src \( -name '*.cpp' -o -name '*.h' \) -print0 | xargs -0 clang-format -i
find src \( -name '*.cpp' -o -name '*.h' \) -print0 | xargs -0 grep -ZlP '\s+$' | xargs -0 -r sed -i 's/[[:space:]]*$//'
clean m=mode:
rm -rf build-{{m}}
-1
View File
@@ -10,7 +10,6 @@
#include <cerrno>
#include <poll.h>
#include <stdexcept>
#include <wayland-client-core.h>
namespace {
+4 -5
View File
@@ -1,13 +1,12 @@
#include "auth/pam_authenticator.h"
#include <pwd.h>
#include <security/pam_appl.h>
#include <sys/types.h>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <pwd.h>
#include <security/pam_appl.h>
#include <sys/types.h>
#include <unistd.h>
#include <vector>
namespace {
+1 -2
View File
@@ -1,11 +1,10 @@
#include "compositors/ext_workspace_backend.h"
#include "core/log.h"
#include "ext-workspace-v1-client-protocol.h"
#include <algorithm>
#include "ext-workspace-v1-client-protocol.h"
namespace {
constexpr Logger kLog("workspace_ext");
@@ -3,35 +3,35 @@
#include "core/log.h"
#include <algorithm>
#include <charconv>
#include <cctype>
#include <cerrno>
#include <charconv>
#include <cstring>
#include <fcntl.h>
#include <filesystem>
#include <format>
#include <string_view>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <fcntl.h>
namespace {
constexpr Logger kLog("workspace_hyprland");
constexpr Logger kLog("workspace_hyprland");
[[nodiscard]] std::optional<std::size_t> parseLeadingNumber(const std::string& value) {
if (value.empty() || !std::isdigit(static_cast<unsigned char>(value.front()))) {
return std::nullopt;
}
[[nodiscard]] std::optional<std::size_t> parseLeadingNumber(const std::string& value) {
if (value.empty() || !std::isdigit(static_cast<unsigned char>(value.front()))) {
return std::nullopt;
}
std::size_t parsed = 0;
std::size_t index = 0;
while (index < value.size() && std::isdigit(static_cast<unsigned char>(value[index]))) {
parsed = (parsed * 10) + static_cast<std::size_t>(value[index] - '0');
++index;
std::size_t parsed = 0;
std::size_t index = 0;
while (index < value.size() && std::isdigit(static_cast<unsigned char>(value[index]))) {
parsed = (parsed * 10) + static_cast<std::size_t>(value[index] - '0');
++index;
}
return parsed > 0 ? std::optional<std::size_t>(parsed) : std::nullopt;
}
return parsed > 0 ? std::optional<std::size_t>(parsed) : std::nullopt;
}
} // namespace
@@ -589,8 +589,7 @@ void HyprlandWorkspaceBackend::handleEvent(std::string_view line) {
}
m_workspaces.erase(std::remove_if(m_workspaces.begin(), m_workspaces.end(),
[&](const WorkspaceState& ws) {
return (id.has_value() && ws.id == *id) ||
(!name.empty() && ws.name == name);
return (id.has_value() && ws.id == *id) || (!name.empty() && ws.name == name);
}),
m_workspaces.end());
@@ -840,8 +839,8 @@ std::string HyprlandWorkspaceBackend::quoteCommandArg(const std::string& value)
}
Workspace HyprlandWorkspaceBackend::toWorkspace(const WorkspaceState& state) {
const std::uint32_t coord = state.id >= 0 ? static_cast<std::uint32_t>(state.id - 1)
: static_cast<std::uint32_t>(state.ordinal);
const std::uint32_t coord =
state.id >= 0 ? static_cast<std::uint32_t>(state.id - 1) : static_cast<std::uint32_t>(state.ordinal);
return Workspace{
.id = !state.name.empty() ? state.name : std::to_string(state.id),
.name = state.name,
@@ -2,10 +2,9 @@
#include "compositors/workspace_backend.h"
#include <json.hpp>
#include <cstdint>
#include <functional>
#include <json.hpp>
#include <optional>
#include <string>
#include <unordered_map>
@@ -1,15 +1,14 @@
#include "compositors/mango/mango_keyboard_backend.h"
#include "core/process.h"
#include "dwl-ipc-unstable-v2-client-protocol.h"
#include "wayland-client-core.h"
#include "wayland-client-protocol.h"
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include "dwl-ipc-unstable-v2-client-protocol.h"
#include "wayland-client-core.h"
#include "wayland-client-protocol.h"
namespace {
constexpr auto kSyncTtl = std::chrono::milliseconds(75);
@@ -111,9 +110,7 @@ MangoKeyboardBackend::MangoKeyboardBackend(std::string_view compositorHint) {
MangoKeyboardBackend::~MangoKeyboardBackend() { cleanup(); }
bool MangoKeyboardBackend::isAvailable() const noexcept {
return syncState() && preferredOutputState() != nullptr;
}
bool MangoKeyboardBackend::isAvailable() const noexcept { return syncState() && preferredOutputState() != nullptr; }
bool MangoKeyboardBackend::cycleLayout() const {
if (!m_enabled) {
@@ -179,7 +176,8 @@ void MangoKeyboardBackend::ensureConnected() const {
}
m_registry = wl_display_get_registry(m_display);
if (m_registry == nullptr || wl_registry_add_listener(m_registry, &kRegistryListener, const_cast<MangoKeyboardBackend*>(this)) != 0) {
if (m_registry == nullptr ||
wl_registry_add_listener(m_registry, &kRegistryListener, const_cast<MangoKeyboardBackend*>(this)) != 0) {
const_cast<MangoKeyboardBackend*>(this)->cleanup();
m_failed = true;
return;
@@ -262,12 +260,14 @@ void MangoKeyboardBackend::onRegistryGlobal(std::uint32_t name, const char* inte
return;
}
if (std::string_view(interfaceName) != zdwl_ipc_manager_v2_interface.name || m_registry == nullptr || m_manager != nullptr) {
if (std::string_view(interfaceName) != zdwl_ipc_manager_v2_interface.name || m_registry == nullptr ||
m_manager != nullptr) {
return;
}
const auto bindVersion = std::min<std::uint32_t>(version, 2);
m_manager = static_cast<zdwl_ipc_manager_v2*>(wl_registry_bind(m_registry, name, &zdwl_ipc_manager_v2_interface, bindVersion));
m_manager = static_cast<zdwl_ipc_manager_v2*>(
wl_registry_bind(m_registry, name, &zdwl_ipc_manager_v2_interface, bindVersion));
if (m_manager == nullptr) {
return;
}
@@ -349,9 +349,8 @@ void MangoKeyboardBackend::bindOutput(std::uint32_t name) {
}
m_outputsByName.emplace(name, output);
m_outputs.try_emplace(output,
OutputState{.pending = {}, .output = output, .handle = nullptr, .active = false,
.keyboardLayout = {}});
m_outputs.try_emplace(
output, OutputState{.pending = {}, .output = output, .handle = nullptr, .active = false, .keyboardLayout = {}});
bindOutputHandle(output);
}
@@ -1,13 +1,12 @@
#include "compositors/mango/mango_workspace_backend.h"
#include "core/log.h"
#include "dwl-ipc-unstable-v2-client-protocol.h"
#include <algorithm>
#include <charconv>
#include <string>
#include "dwl-ipc-unstable-v2-client-protocol.h"
namespace {
constexpr Logger kLog("workspace_mango");
@@ -230,8 +229,7 @@ void MangoWorkspaceBackend::onOutputTag(zdwl_ipc_output_v2* handle, std::uint32_
tagInfo.occupied = clients > 0;
kLog.debug("tag event output={} protocol_tag={} active={} urgent={} occupied={} total_tags={} snapshot={}",
static_cast<const void*>(output->second.output), tag + 1, tagInfo.active ? "yes" : "no",
tagInfo.urgent ? "yes" : "no", tagInfo.occupied ? "yes" : "no", m_tagCount,
summarizeTags(output->second));
tagInfo.urgent ? "yes" : "no", tagInfo.occupied ? "yes" : "no", m_tagCount, summarizeTags(output->second));
}
void MangoWorkspaceBackend::onOutputFrame(zdwl_ipc_output_v2* handle) {
+74 -74
View File
@@ -14,103 +14,103 @@
namespace {
constexpr Logger kLog("niri_output");
constexpr Logger kLog("niri_output");
[[nodiscard]] bool containsToken(std::string_view haystack, std::string_view needle) {
if (haystack.empty() || needle.empty()) {
return false;
}
std::string lhs(haystack);
std::string rhs(needle);
std::ranges::transform(lhs, lhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
std::ranges::transform(rhs, rhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return lhs.find(rhs) != std::string::npos;
}
[[nodiscard]] std::string trim(std::string value) {
auto notSpace = [](unsigned char c) { return !std::isspace(c); };
const auto begin = std::find_if(value.begin(), value.end(), notSpace);
if (begin == value.end()) {
return {};
}
const auto end = std::find_if(value.rbegin(), value.rend(), notSpace).base();
return std::string(begin, end);
}
[[nodiscard]] std::string readCommand(const char* cmd) {
FILE* pipe = ::popen(cmd, "r");
if (pipe == nullptr) {
return {};
[[nodiscard]] bool containsToken(std::string_view haystack, std::string_view needle) {
if (haystack.empty() || needle.empty()) {
return false;
}
std::string lhs(haystack);
std::string rhs(needle);
std::ranges::transform(lhs, lhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
std::ranges::transform(rhs, rhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return lhs.find(rhs) != std::string::npos;
}
std::array<char, 512> buffer{};
std::string output;
while (std::fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
output += buffer.data();
}
::pclose(pipe);
return trim(output);
}
[[nodiscard]] std::optional<std::string> parseFocusedOutputName(std::string_view payload) {
if (payload.empty()) {
return std::nullopt;
[[nodiscard]] std::string trim(std::string value) {
auto notSpace = [](unsigned char c) { return !std::isspace(c); };
const auto begin = std::find_if(value.begin(), value.end(), notSpace);
if (begin == value.end()) {
return {};
}
const auto end = std::find_if(value.rbegin(), value.rend(), notSpace).base();
return std::string(begin, end);
}
try {
const auto json = nlohmann::json::parse(payload);
if (json.is_string()) {
const auto value = trim(json.get<std::string>());
return value.empty() ? std::nullopt : std::optional<std::string>{value};
[[nodiscard]] std::string readCommand(const char* cmd) {
FILE* pipe = ::popen(cmd, "r");
if (pipe == nullptr) {
return {};
}
if (json.is_object()) {
if (auto it = json.find("name"); it != json.end() && it->is_string()) {
const auto value = trim(it->get<std::string>());
if (!value.empty()) {
return value;
}
std::array<char, 512> buffer{};
std::string output;
while (std::fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
output += buffer.data();
}
::pclose(pipe);
return trim(output);
}
[[nodiscard]] std::optional<std::string> parseFocusedOutputName(std::string_view payload) {
if (payload.empty()) {
return std::nullopt;
}
try {
const auto json = nlohmann::json::parse(payload);
if (json.is_string()) {
const auto value = trim(json.get<std::string>());
return value.empty() ? std::nullopt : std::optional<std::string>{value};
}
if (auto it = json.find("output"); it != json.end()) {
if (it->is_string()) {
if (json.is_object()) {
if (auto it = json.find("name"); it != json.end() && it->is_string()) {
const auto value = trim(it->get<std::string>());
if (!value.empty()) {
return value;
}
}
if (it->is_object()) {
if (auto nameIt = it->find("name"); nameIt != it->end() && nameIt->is_string()) {
const auto value = trim(nameIt->get<std::string>());
if (auto it = json.find("output"); it != json.end()) {
if (it->is_string()) {
const auto value = trim(it->get<std::string>());
if (!value.empty()) {
return value;
}
}
if (it->is_object()) {
if (auto nameIt = it->find("name"); nameIt != it->end() && nameIt->is_string()) {
const auto value = trim(nameIt->get<std::string>());
if (!value.empty()) {
return value;
}
}
}
}
}
if (json.is_array()) {
for (const auto& item : json) {
if (!item.is_object()) {
continue;
}
if (auto it = item.find("name"); it != item.end() && it->is_string()) {
const auto value = trim(it->get<std::string>());
if (!value.empty()) {
return value;
}
}
}
}
} catch (const nlohmann::json::exception&) {
const auto value = trim(std::string(payload));
return value.empty() ? std::nullopt : std::optional<std::string>{value};
}
if (json.is_array()) {
for (const auto& item : json) {
if (!item.is_object()) {
continue;
}
if (auto it = item.find("name"); it != item.end() && it->is_string()) {
const auto value = trim(it->get<std::string>());
if (!value.empty()) {
return value;
}
}
}
}
} catch (const nlohmann::json::exception&) {
const auto value = trim(std::string(payload));
return value.empty() ? std::nullopt : std::optional<std::string>{value};
return std::nullopt;
}
return std::nullopt;
}
} // namespace
NiriOutputBackend::NiriOutputBackend(std::string_view compositorHint) {
+67 -67
View File
@@ -4,9 +4,9 @@
#include <algorithm>
#include <array>
#include <charconv>
#include <cerrno>
#include <cctype>
#include <cerrno>
#include <charconv>
#include <cstring>
#include <fcntl.h>
#include <json.hpp>
@@ -16,73 +16,73 @@
namespace {
constexpr Logger kLog("niri_workspace");
constexpr auto kReconnectDelay = std::chrono::seconds(2);
constexpr std::string_view kEventStreamRequest = "\"EventStream\"\n";
constexpr Logger kLog("niri_workspace");
constexpr auto kReconnectDelay = std::chrono::seconds(2);
constexpr std::string_view kEventStreamRequest = "\"EventStream\"\n";
[[nodiscard]] bool containsToken(std::string_view haystack, std::string_view needle) {
if (haystack.empty() || needle.empty()) {
return false;
}
std::string lhs(haystack);
std::string rhs(needle);
std::ranges::transform(lhs, lhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
std::ranges::transform(rhs, rhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return lhs.find(rhs) != std::string::npos;
}
[[nodiscard]] std::optional<std::uint64_t> jsonUnsigned(const nlohmann::json& json) {
if (json.is_number_unsigned()) {
return json.get<std::uint64_t>();
}
if (json.is_number_integer()) {
const auto value = json.get<std::int64_t>();
if (value >= 0) {
return static_cast<std::uint64_t>(value);
[[nodiscard]] bool containsToken(std::string_view haystack, std::string_view needle) {
if (haystack.empty() || needle.empty()) {
return false;
}
std::string lhs(haystack);
std::string rhs(needle);
std::ranges::transform(lhs, lhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
std::ranges::transform(rhs, rhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return lhs.find(rhs) != std::string::npos;
}
return std::nullopt;
}
[[nodiscard]] std::optional<std::uint64_t> jsonOptionalUnsigned(const nlohmann::json& json, const char* key) {
const auto it = json.find(key);
if (it == json.end() || it->is_null()) {
[[nodiscard]] std::optional<std::uint64_t> jsonUnsigned(const nlohmann::json& json) {
if (json.is_number_unsigned()) {
return json.get<std::uint64_t>();
}
if (json.is_number_integer()) {
const auto value = json.get<std::int64_t>();
if (value >= 0) {
return static_cast<std::uint64_t>(value);
}
}
return std::nullopt;
}
return jsonUnsigned(*it);
}
[[nodiscard]] std::string jsonOptionalString(const nlohmann::json& json, const char* key) {
const auto it = json.find(key);
if (it == json.end() || !it->is_string()) {
return {};
}
return it->get<std::string>();
}
[[nodiscard]] const nlohmann::json* arrayPayload(const nlohmann::json& payload, const char* key) {
if (payload.is_array()) {
return &payload;
}
const auto it = payload.find(key);
if (payload.is_object() && it != payload.end() && it->is_array()) {
return &(*it);
}
return nullptr;
}
[[nodiscard]] const nlohmann::json* objectPayload(const nlohmann::json& payload, const char* key) {
if (payload.is_object()) {
const auto it = payload.find(key);
if (it != payload.end() && it->is_object()) {
return &(*it);
[[nodiscard]] std::optional<std::uint64_t> jsonOptionalUnsigned(const nlohmann::json& json, const char* key) {
const auto it = json.find(key);
if (it == json.end() || it->is_null()) {
return std::nullopt;
}
if (payload.contains("id")) {
return jsonUnsigned(*it);
}
[[nodiscard]] std::string jsonOptionalString(const nlohmann::json& json, const char* key) {
const auto it = json.find(key);
if (it == json.end() || !it->is_string()) {
return {};
}
return it->get<std::string>();
}
[[nodiscard]] const nlohmann::json* arrayPayload(const nlohmann::json& payload, const char* key) {
if (payload.is_array()) {
return &payload;
}
const auto it = payload.find(key);
if (payload.is_object() && it != payload.end() && it->is_array()) {
return &(*it);
}
return nullptr;
}
[[nodiscard]] const nlohmann::json* objectPayload(const nlohmann::json& payload, const char* key) {
if (payload.is_object()) {
const auto it = payload.find(key);
if (it != payload.end() && it->is_object()) {
return &(*it);
}
if (payload.contains("id")) {
return &payload;
}
}
return nullptr;
}
return nullptr;
}
} // namespace
@@ -111,9 +111,9 @@ int NiriWorkspaceMonitor::pollTimeoutMs() const noexcept {
return 0;
}
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(m_nextReconnectAt -
std::chrono::steady_clock::now())
.count();
const auto remaining =
std::chrono::duration_cast<std::chrono::milliseconds>(m_nextReconnectAt - std::chrono::steady_clock::now())
.count();
return static_cast<int>(std::max<std::int64_t>(0, remaining));
}
@@ -189,13 +189,11 @@ void NiriWorkspaceMonitor::apply(std::vector<Workspace>& workspaces, const std::
matches[i] = pickCandidate([&](const WorkspaceState& candidate) { return candidate.id == *parsedId; });
}
if (matches[i] == nullptr && !workspaces[i].name.empty()) {
matches[i] =
pickCandidate([&](const WorkspaceState& candidate) { return candidate.name == workspaces[i].name; });
matches[i] = pickCandidate([&](const WorkspaceState& candidate) { return candidate.name == workspaces[i].name; });
}
if (matches[i] == nullptr && parsedIndex.has_value()) {
matches[i] = pickCandidate([&](const WorkspaceState& candidate) {
return static_cast<std::size_t>(candidate.idx) == *parsedIndex;
});
matches[i] = pickCandidate(
[&](const WorkspaceState& candidate) { return static_cast<std::size_t>(candidate.idx) == *parsedIndex; });
}
}
@@ -293,7 +291,9 @@ void NiriWorkspaceMonitor::closeSocket(bool scheduleReconnectFlag) {
}
}
void NiriWorkspaceMonitor::scheduleReconnect() { m_nextReconnectAt = std::chrono::steady_clock::now() + kReconnectDelay; }
void NiriWorkspaceMonitor::scheduleReconnect() {
m_nextReconnectAt = std::chrono::steady_clock::now() + kReconnectDelay;
}
void NiriWorkspaceMonitor::readSocket() {
std::array<char, 4096> buffer{};
@@ -2,11 +2,10 @@
#include "compositors/workspace_backend.h"
#include <json.hpp>
#include <chrono>
#include <cstdint>
#include <functional>
#include <json.hpp>
#include <optional>
#include <poll.h>
#include <string>
@@ -60,8 +59,7 @@ private:
[[nodiscard]] bool handleWindowOpenedOrChanged(const nlohmann::json& payload);
[[nodiscard]] bool handleWindowClosed(const nlohmann::json& payload);
[[nodiscard]] static std::optional<WorkspaceState> parseWorkspace(const nlohmann::json& json);
[[nodiscard]] static std::optional<std::pair<std::uint64_t, WindowState>>
parseWindow(const nlohmann::json& json);
[[nodiscard]] static std::optional<std::pair<std::uint64_t, WindowState>> parseWindow(const nlohmann::json& json);
[[nodiscard]] static std::optional<std::uint64_t> parseUnsigned(const std::string& value);
[[nodiscard]] static std::optional<std::size_t> parseLeadingNumber(const std::string& value);
void recomputeOccupancy();
+1 -2
View File
@@ -1,10 +1,9 @@
#pragma once
#include "core/toml.h"
#include "ui/palette.h"
#include "ui/style.h"
#include "core/toml.h"
#include <array>
#include <cstdint>
#include <functional>
+1
View File
@@ -1,4 +1,5 @@
#include "core/ui_phase.h"
#include "core/log.h"
#include <cstdlib>
+50 -57
View File
@@ -13,17 +13,17 @@
namespace {
constexpr Logger kLog("bluetooth");
constexpr Logger kLog("bluetooth");
const sdbus::ServiceName k_bluezBusName{"org.bluez"};
const sdbus::ObjectPath k_bluezRoot{"/org/bluez"};
const sdbus::ObjectPath k_agentObjectPath{"/org/noctalia/BluetoothAgent"};
constexpr auto k_agentInterface = "org.bluez.Agent1";
constexpr auto k_agentManagerInterface = "org.bluez.AgentManager1";
constexpr auto k_capability = "KeyboardDisplay";
const sdbus::ServiceName k_bluezBusName{"org.bluez"};
const sdbus::ObjectPath k_bluezRoot{"/org/bluez"};
const sdbus::ObjectPath k_agentObjectPath{"/org/noctalia/BluetoothAgent"};
constexpr auto k_agentInterface = "org.bluez.Agent1";
constexpr auto k_agentManagerInterface = "org.bluez.AgentManager1";
constexpr auto k_capability = "KeyboardDisplay";
const sdbus::Error::Name k_errRejected{"org.bluez.Error.Rejected"};
const sdbus::Error::Name k_errCanceled{"org.bluez.Error.Canceled"};
const sdbus::Error::Name k_errRejected{"org.bluez.Error.Rejected"};
const sdbus::Error::Name k_errCanceled{"org.bluez.Error.Canceled"};
} // namespace
@@ -182,48 +182,45 @@ BluetoothAgent::BluetoothAgent(SystemBus& bus) : m_impl(std::make_unique<Impl>(b
m_impl->object = sdbus::createObject(bus.connection(), k_agentObjectPath);
m_impl->object
->addVTable(
sdbus::registerMethod("Release").implementedAs([]() {}),
sdbus::registerMethod("RequestPinCode")
.withInputParamNames("device")
.withOutputParamNames("pincode")
.implementedAs([this](sdbus::Result<std::string>&& result, sdbus::ObjectPath device) {
m_impl->onRequestPinCode(std::move(result), std::move(device));
}),
sdbus::registerMethod("DisplayPinCode")
.withInputParamNames("device", "pincode")
.implementedAs(
[this](sdbus::Result<>&& result, sdbus::ObjectPath device, std::string pincode) {
m_impl->onDisplayPinCode(std::move(result), std::move(device), std::move(pincode));
}),
sdbus::registerMethod("RequestPasskey")
.withInputParamNames("device")
.withOutputParamNames("passkey")
.implementedAs([this](sdbus::Result<std::uint32_t>&& result, sdbus::ObjectPath device) {
m_impl->onRequestPasskey(std::move(result), std::move(device));
}),
sdbus::registerMethod("DisplayPasskey")
.withInputParamNames("device", "passkey", "entered")
.implementedAs([this](sdbus::ObjectPath device, std::uint32_t passkey, std::uint16_t entered) {
m_impl->onDisplayPasskey(std::move(device), passkey, entered);
}),
sdbus::registerMethod("RequestConfirmation")
.withInputParamNames("device", "passkey")
.implementedAs(
[this](sdbus::Result<>&& result, sdbus::ObjectPath device, std::uint32_t passkey) {
m_impl->onRequestConfirmation(std::move(result), std::move(device), passkey);
}),
sdbus::registerMethod("RequestAuthorization")
.withInputParamNames("device")
.implementedAs([this](sdbus::Result<>&& result, sdbus::ObjectPath device) {
m_impl->onRequestAuthorization(std::move(result), std::move(device));
}),
sdbus::registerMethod("AuthorizeService")
.withInputParamNames("device", "uuid")
.implementedAs([this](sdbus::Result<>&& result, sdbus::ObjectPath device, std::string uuid) {
m_impl->onAuthorizeService(std::move(result), std::move(device), std::move(uuid));
}),
sdbus::registerMethod("Cancel").implementedAs([this]() { m_impl->onCancel(); }))
->addVTable(sdbus::registerMethod("Release").implementedAs([]() {}),
sdbus::registerMethod("RequestPinCode")
.withInputParamNames("device")
.withOutputParamNames("pincode")
.implementedAs([this](sdbus::Result<std::string>&& result, sdbus::ObjectPath device) {
m_impl->onRequestPinCode(std::move(result), std::move(device));
}),
sdbus::registerMethod("DisplayPinCode")
.withInputParamNames("device", "pincode")
.implementedAs([this](sdbus::Result<>&& result, sdbus::ObjectPath device, std::string pincode) {
m_impl->onDisplayPinCode(std::move(result), std::move(device), std::move(pincode));
}),
sdbus::registerMethod("RequestPasskey")
.withInputParamNames("device")
.withOutputParamNames("passkey")
.implementedAs([this](sdbus::Result<std::uint32_t>&& result, sdbus::ObjectPath device) {
m_impl->onRequestPasskey(std::move(result), std::move(device));
}),
sdbus::registerMethod("DisplayPasskey")
.withInputParamNames("device", "passkey", "entered")
.implementedAs([this](sdbus::ObjectPath device, std::uint32_t passkey, std::uint16_t entered) {
m_impl->onDisplayPasskey(std::move(device), passkey, entered);
}),
sdbus::registerMethod("RequestConfirmation")
.withInputParamNames("device", "passkey")
.implementedAs([this](sdbus::Result<>&& result, sdbus::ObjectPath device, std::uint32_t passkey) {
m_impl->onRequestConfirmation(std::move(result), std::move(device), passkey);
}),
sdbus::registerMethod("RequestAuthorization")
.withInputParamNames("device")
.implementedAs([this](sdbus::Result<>&& result, sdbus::ObjectPath device) {
m_impl->onRequestAuthorization(std::move(result), std::move(device));
}),
sdbus::registerMethod("AuthorizeService")
.withInputParamNames("device", "uuid")
.implementedAs([this](sdbus::Result<>&& result, sdbus::ObjectPath device, std::string uuid) {
m_impl->onAuthorizeService(std::move(result), std::move(device), std::move(uuid));
}),
sdbus::registerMethod("Cancel").implementedAs([this]() { m_impl->onCancel(); }))
.forInterface(k_agentInterface);
try {
@@ -247,9 +244,7 @@ BluetoothAgent::~BluetoothAgent() {
m_impl->cancelPending();
try {
auto agentManager = sdbus::createProxy(m_impl->bus.connection(), k_bluezBusName, k_bluezRoot);
agentManager->callMethod("UnregisterAgent")
.onInterface(k_agentManagerInterface)
.withArguments(k_agentObjectPath);
agentManager->callMethod("UnregisterAgent").onInterface(k_agentManagerInterface).withArguments(k_agentObjectPath);
} catch (const sdbus::Error& e) {
kLog.debug("BlueZ agent unregister failed: {}", e.what());
}
@@ -299,9 +294,7 @@ void BluetoothAgent::cancelPending() {
}
}
bool BluetoothAgent::hasPendingRequest() const noexcept {
return m_impl != nullptr && m_impl->hasPending();
}
bool BluetoothAgent::hasPendingRequest() const noexcept { return m_impl != nullptr && m_impl->hasPending(); }
BluetoothPairingRequest BluetoothAgent::pendingRequest() const {
return m_impl != nullptr ? m_impl->pending : BluetoothPairingRequest{};
+2 -2
View File
@@ -47,9 +47,9 @@ public:
void setRequestCallback(RequestCallback callback);
// Reply paths — safe no-ops if nothing is pending.
void acceptConfirm(); // Confirm / Authorize / AuthorizeService / DisplayPinCode / DisplayPasskey
void acceptConfirm(); // Confirm / Authorize / AuthorizeService / DisplayPinCode / DisplayPasskey
void rejectConfirm();
void submitPin(const std::string& pin); // PinCode kind
void submitPin(const std::string& pin); // PinCode kind
void submitPasskey(std::uint32_t passkey); // Passkey kind
void cancelPending();
+176 -177
View File
@@ -15,211 +15,210 @@
namespace {
constexpr Logger kLog("bluetooth");
constexpr Logger kLog("bluetooth");
const sdbus::ServiceName k_bluezBusName{"org.bluez"};
const sdbus::ObjectPath k_rootPath{"/"};
constexpr auto k_adapterInterface = "org.bluez.Adapter1";
constexpr auto k_deviceInterface = "org.bluez.Device1";
constexpr auto k_batteryInterface = "org.bluez.Battery1";
constexpr auto k_objectManagerInterface = "org.freedesktop.DBus.ObjectManager";
constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
const sdbus::ServiceName k_bluezBusName{"org.bluez"};
const sdbus::ObjectPath k_rootPath{"/"};
constexpr auto k_adapterInterface = "org.bluez.Adapter1";
constexpr auto k_deviceInterface = "org.bluez.Device1";
constexpr auto k_batteryInterface = "org.bluez.Battery1";
constexpr auto k_objectManagerInterface = "org.freedesktop.DBus.ObjectManager";
constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
using InterfaceProps = std::map<std::string, sdbus::Variant>;
using ObjectInterfaces = std::map<std::string, InterfaceProps>;
using ManagedObjects = std::map<sdbus::ObjectPath, ObjectInterfaces>;
using InterfaceProps = std::map<std::string, sdbus::Variant>;
using ObjectInterfaces = std::map<std::string, InterfaceProps>;
using ManagedObjects = std::map<sdbus::ObjectPath, ObjectInterfaces>;
template <typename T>
std::optional<T> variantGet(const sdbus::Variant& value) {
try {
return value.get<T>();
} catch (const sdbus::Error&) {
return std::nullopt;
template <typename T> std::optional<T> variantGet(const sdbus::Variant& value) {
try {
return value.get<T>();
} catch (const sdbus::Error&) {
return std::nullopt;
}
}
}
BluetoothDeviceKind classifyIcon(std::string_view icon) {
// See https://specifications.freedesktop.org/icon-naming-spec/latest/
// BlueZ uses names like "audio-headset", "input-mouse", "phone", "computer".
if (icon == "audio-headset") {
return BluetoothDeviceKind::Headset;
}
if (icon == "audio-headphones") {
return BluetoothDeviceKind::Headphones;
}
if (icon == "audio-card" || icon == "audio-speakers") {
return BluetoothDeviceKind::Speaker;
}
if (icon == "input-mouse") {
return BluetoothDeviceKind::Mouse;
}
if (icon == "input-keyboard") {
return BluetoothDeviceKind::Keyboard;
}
if (icon == "input-gaming") {
return BluetoothDeviceKind::Gamepad;
}
if (icon == "phone") {
return BluetoothDeviceKind::Phone;
}
if (icon == "computer") {
return BluetoothDeviceKind::Computer;
}
if (icon == "video-display") {
return BluetoothDeviceKind::Tv;
}
return BluetoothDeviceKind::Unknown;
}
BluetoothDeviceKind classifyClass(std::uint32_t cod) {
// Bluetooth Class of Device: bits 8-12 are Major Device Class.
const std::uint32_t major = (cod >> 8) & 0x1FU;
const std::uint32_t minor = (cod >> 2) & 0x3FU;
switch (major) {
case 0x01: // Computer
return BluetoothDeviceKind::Computer;
case 0x02: // Phone
return BluetoothDeviceKind::Phone;
case 0x04: // Audio/Video
switch (minor) {
case 0x01: // Wearable headset
case 0x02: // Hands-free
BluetoothDeviceKind classifyIcon(std::string_view icon) {
// See https://specifications.freedesktop.org/icon-naming-spec/latest/
// BlueZ uses names like "audio-headset", "input-mouse", "phone", "computer".
if (icon == "audio-headset") {
return BluetoothDeviceKind::Headset;
case 0x06:
return BluetoothDeviceKind::Headphones;
case 0x05: // Loudspeaker
case 0x07: // Portable audio
return BluetoothDeviceKind::Speaker;
case 0x04:
return BluetoothDeviceKind::Microphone;
case 0x0A: // Video display/loudspeaker
case 0x0B: // Video display/conferencing
return BluetoothDeviceKind::Tv;
default:
}
if (icon == "audio-headphones") {
return BluetoothDeviceKind::Headphones;
}
case 0x05: // Peripheral
switch (minor & 0x0FU) {
case 0x01:
return BluetoothDeviceKind::Keyboard;
case 0x02:
if (icon == "audio-card" || icon == "audio-speakers") {
return BluetoothDeviceKind::Speaker;
}
if (icon == "input-mouse") {
return BluetoothDeviceKind::Mouse;
}
if (icon == "input-keyboard") {
return BluetoothDeviceKind::Keyboard;
}
if (icon == "input-gaming") {
return BluetoothDeviceKind::Gamepad;
}
if (icon == "phone") {
return BluetoothDeviceKind::Phone;
}
if (icon == "computer") {
return BluetoothDeviceKind::Computer;
}
if (icon == "video-display") {
return BluetoothDeviceKind::Tv;
}
return BluetoothDeviceKind::Unknown;
}
BluetoothDeviceKind classifyClass(std::uint32_t cod) {
// Bluetooth Class of Device: bits 8-12 are Major Device Class.
const std::uint32_t major = (cod >> 8) & 0x1FU;
const std::uint32_t minor = (cod >> 2) & 0x3FU;
switch (major) {
case 0x01: // Computer
return BluetoothDeviceKind::Computer;
case 0x02: // Phone
return BluetoothDeviceKind::Phone;
case 0x04: // Audio/Video
switch (minor) {
case 0x01: // Wearable headset
case 0x02: // Hands-free
return BluetoothDeviceKind::Headset;
case 0x06:
return BluetoothDeviceKind::Headphones;
case 0x05: // Loudspeaker
case 0x07: // Portable audio
return BluetoothDeviceKind::Speaker;
case 0x04:
return BluetoothDeviceKind::Microphone;
case 0x0A: // Video display/loudspeaker
case 0x0B: // Video display/conferencing
return BluetoothDeviceKind::Tv;
default:
return BluetoothDeviceKind::Headphones;
}
case 0x05: // Peripheral
switch (minor & 0x0FU) {
case 0x01:
return BluetoothDeviceKind::Keyboard;
case 0x02:
return BluetoothDeviceKind::Mouse;
default:
return BluetoothDeviceKind::Unknown;
}
case 0x07: // Wearable
return BluetoothDeviceKind::Watch;
case 0x08: // Toy / Gamepad
return BluetoothDeviceKind::Gamepad;
default:
return BluetoothDeviceKind::Unknown;
}
case 0x07: // Wearable
return BluetoothDeviceKind::Watch;
case 0x08: // Toy / Gamepad
return BluetoothDeviceKind::Gamepad;
default:
return BluetoothDeviceKind::Unknown;
}
}
void readAdapterProps(const InterfaceProps& props, BluetoothState& out) {
out.adapterPresent = true;
if (auto it = props.find("Powered"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.powered = *v;
void readAdapterProps(const InterfaceProps& props, BluetoothState& out) {
out.adapterPresent = true;
if (auto it = props.find("Powered"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.powered = *v;
}
}
if (auto it = props.find("Discoverable"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.discoverable = *v;
}
}
if (auto it = props.find("Pairable"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.pairable = *v;
}
}
if (auto it = props.find("Discovering"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.discovering = *v;
}
}
if (auto it = props.find("Alias"); it != props.end()) {
if (auto v = variantGet<std::string>(it->second)) {
out.adapterName = std::move(*v);
}
} else if (auto nameIt = props.find("Name"); nameIt != props.end()) {
if (auto v = variantGet<std::string>(nameIt->second)) {
out.adapterName = std::move(*v);
}
}
}
if (auto it = props.find("Discoverable"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.discoverable = *v;
}
}
if (auto it = props.find("Pairable"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.pairable = *v;
}
}
if (auto it = props.find("Discovering"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.discovering = *v;
}
}
if (auto it = props.find("Alias"); it != props.end()) {
if (auto v = variantGet<std::string>(it->second)) {
out.adapterName = std::move(*v);
}
} else if (auto nameIt = props.find("Name"); nameIt != props.end()) {
if (auto v = variantGet<std::string>(nameIt->second)) {
out.adapterName = std::move(*v);
}
}
}
void mergeDeviceProps(const InterfaceProps& props, BluetoothDeviceInfo& out) {
if (auto it = props.find("Address"); it != props.end()) {
if (auto v = variantGet<std::string>(it->second)) {
out.address = std::move(*v);
void mergeDeviceProps(const InterfaceProps& props, BluetoothDeviceInfo& out) {
if (auto it = props.find("Address"); it != props.end()) {
if (auto v = variantGet<std::string>(it->second)) {
out.address = std::move(*v);
}
}
}
if (auto it = props.find("Alias"); it != props.end()) {
if (auto v = variantGet<std::string>(it->second)) {
out.alias = std::move(*v);
}
}
if (out.alias.empty()) {
if (auto it = props.find("Name"); it != props.end()) {
if (auto it = props.find("Alias"); it != props.end()) {
if (auto v = variantGet<std::string>(it->second)) {
out.alias = std::move(*v);
}
}
}
if (auto it = props.find("Paired"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.paired = *v;
}
}
if (auto it = props.find("Trusted"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.trusted = *v;
}
}
if (auto it = props.find("Connected"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.connected = *v;
if (out.connected) {
out.connecting = false;
if (out.alias.empty()) {
if (auto it = props.find("Name"); it != props.end()) {
if (auto v = variantGet<std::string>(it->second)) {
out.alias = std::move(*v);
}
}
}
}
if (auto it = props.find("RSSI"); it != props.end()) {
if (auto v = variantGet<std::int16_t>(it->second)) {
out.rssi = *v;
out.hasRssi = true;
}
}
BluetoothDeviceKind kindFromIcon = BluetoothDeviceKind::Unknown;
if (auto it = props.find("Icon"); it != props.end()) {
if (auto v = variantGet<std::string>(it->second)) {
kindFromIcon = classifyIcon(*v);
}
}
if (kindFromIcon != BluetoothDeviceKind::Unknown) {
out.kind = kindFromIcon;
} else if (auto it = props.find("Class"); it != props.end()) {
if (auto v = variantGet<std::uint32_t>(it->second)) {
const auto kindFromCod = classifyClass(*v);
if (kindFromCod != BluetoothDeviceKind::Unknown) {
out.kind = kindFromCod;
if (auto it = props.find("Paired"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.paired = *v;
}
}
if (auto it = props.find("Trusted"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.trusted = *v;
}
}
if (auto it = props.find("Connected"); it != props.end()) {
if (auto v = variantGet<bool>(it->second)) {
out.connected = *v;
if (out.connected) {
out.connecting = false;
}
}
}
if (auto it = props.find("RSSI"); it != props.end()) {
if (auto v = variantGet<std::int16_t>(it->second)) {
out.rssi = *v;
out.hasRssi = true;
}
}
BluetoothDeviceKind kindFromIcon = BluetoothDeviceKind::Unknown;
if (auto it = props.find("Icon"); it != props.end()) {
if (auto v = variantGet<std::string>(it->second)) {
kindFromIcon = classifyIcon(*v);
}
}
if (kindFromIcon != BluetoothDeviceKind::Unknown) {
out.kind = kindFromIcon;
} else if (auto it = props.find("Class"); it != props.end()) {
if (auto v = variantGet<std::uint32_t>(it->second)) {
const auto kindFromCod = classifyClass(*v);
if (kindFromCod != BluetoothDeviceKind::Unknown) {
out.kind = kindFromCod;
}
}
}
if (out.alias.empty()) {
out.alias = out.address.empty() ? std::string("Unknown device") : out.address;
}
}
if (out.alias.empty()) {
out.alias = out.address.empty() ? std::string("Unknown device") : out.address;
}
}
void mergeBatteryProps(const InterfaceProps& props, BluetoothDeviceInfo& out) {
if (auto it = props.find("Percentage"); it != props.end()) {
if (auto v = variantGet<std::uint8_t>(it->second)) {
out.hasBattery = true;
out.batteryPercent = *v;
void mergeBatteryProps(const InterfaceProps& props, BluetoothDeviceInfo& out) {
if (auto it = props.find("Percentage"); it != props.end()) {
if (auto v = variantGet<std::uint8_t>(it->second)) {
out.hasBattery = true;
out.batteryPercent = *v;
}
}
}
}
} // namespace
@@ -613,8 +612,8 @@ void BluetoothService::forget(const std::string& devicePath) {
}
BluetoothDeviceInfo* BluetoothService::findDevice(const std::string& path) {
auto it = std::find_if(m_devices.begin(), m_devices.end(),
[&](const BluetoothDeviceInfo& d) { return d.path == path; });
auto it =
std::find_if(m_devices.begin(), m_devices.end(), [&](const BluetoothDeviceInfo& d) { return d.path == path; });
return it == m_devices.end() ? nullptr : &*it;
}
+1 -1
View File
@@ -10,7 +10,7 @@
class SystemBus;
namespace sdbus {
class IProxy;
class IProxy;
}
enum class BluetoothDeviceKind : std::uint8_t {
+143 -144
View File
@@ -30,177 +30,176 @@ std::string joinedArtists(const std::vector<std::string>& artists) {
namespace {
static constexpr auto k_dbus_interface = "org.freedesktop.DBus";
static constexpr auto k_properties_interface = "org.freedesktop.DBus.Properties";
static constexpr auto k_mpris_root_interface = "org.mpris.MediaPlayer2";
static constexpr auto k_mpris_player_interface = "org.mpris.MediaPlayer2.Player";
static constexpr auto k_noctalia_mpris_interface = "dev.noctalia.Mpris";
static constexpr auto k_properties_debounce_window = std::chrono::milliseconds{120};
static constexpr auto k_metadata_stabilize_window = std::chrono::milliseconds{900};
static const sdbus::ServiceName k_dbus_name{"org.freedesktop.DBus"};
static const sdbus::ObjectPath k_dbus_path{"/org/freedesktop/DBus"};
static const sdbus::ObjectPath k_mpris_path{"/org/mpris/MediaPlayer2"};
static const sdbus::ServiceName k_noctalia_mpris_bus_name{"dev.noctalia.Mpris"};
static const sdbus::ObjectPath k_noctalia_mpris_object_path{"/dev/noctalia/Mpris"};
static constexpr auto k_dbus_interface = "org.freedesktop.DBus";
static constexpr auto k_properties_interface = "org.freedesktop.DBus.Properties";
static constexpr auto k_mpris_root_interface = "org.mpris.MediaPlayer2";
static constexpr auto k_mpris_player_interface = "org.mpris.MediaPlayer2.Player";
static constexpr auto k_noctalia_mpris_interface = "dev.noctalia.Mpris";
static constexpr auto k_properties_debounce_window = std::chrono::milliseconds{120};
static constexpr auto k_metadata_stabilize_window = std::chrono::milliseconds{900};
static const sdbus::ServiceName k_dbus_name{"org.freedesktop.DBus"};
static const sdbus::ObjectPath k_dbus_path{"/org/freedesktop/DBus"};
static const sdbus::ObjectPath k_mpris_path{"/org/mpris/MediaPlayer2"};
static const sdbus::ServiceName k_noctalia_mpris_bus_name{"dev.noctalia.Mpris"};
static const sdbus::ObjectPath k_noctalia_mpris_object_path{"/dev/noctalia/Mpris"};
bool is_mpris_bus_name(std::string_view name) { return name.starts_with("org.mpris.MediaPlayer2."); }
bool is_mpris_bus_name(std::string_view name) { return name.starts_with("org.mpris.MediaPlayer2."); }
bool is_valid_loop_status(std::string_view loop_status) {
return loop_status == "None" || loop_status == "Track" || loop_status == "Playlist";
}
template <typename T>
T get_property_or(sdbus::IProxy& proxy, std::string_view interface_name, std::string_view property_name, T fallback) {
try {
const sdbus::Variant value = proxy.getProperty(property_name).onInterface(interface_name);
return value.get<T>();
} catch (const sdbus::Error&) {
return fallback;
}
}
std::map<std::string, sdbus::Variant> get_metadata_or(sdbus::IProxy& proxy) {
return get_property_or(proxy, k_mpris_player_interface, "Metadata", std::map<std::string, sdbus::Variant>{});
}
std::string get_string_from_variant(const std::map<std::string, sdbus::Variant>& values, std::string_view key) {
const auto it = values.find(std::string{key});
if (it == values.end()) {
return {};
bool is_valid_loop_status(std::string_view loop_status) {
return loop_status == "None" || loop_status == "Track" || loop_status == "Playlist";
}
try {
return it->second.get<std::string>();
} catch (const sdbus::Error&) {
return {};
}
}
std::string get_object_path_from_variant(const std::map<std::string, sdbus::Variant>& values, std::string_view key) {
const auto it = values.find(std::string{key});
if (it == values.end()) {
return {};
template <typename T>
T get_property_or(sdbus::IProxy& proxy, std::string_view interface_name, std::string_view property_name, T fallback) {
try {
const sdbus::Variant value = proxy.getProperty(property_name).onInterface(interface_name);
return value.get<T>();
} catch (const sdbus::Error&) {
return fallback;
}
}
try {
return it->second.get<sdbus::ObjectPath>();
} catch (const sdbus::Error&) {
std::map<std::string, sdbus::Variant> get_metadata_or(sdbus::IProxy& proxy) {
return get_property_or(proxy, k_mpris_player_interface, "Metadata", std::map<std::string, sdbus::Variant>{});
}
std::string get_string_from_variant(const std::map<std::string, sdbus::Variant>& values, std::string_view key) {
const auto it = values.find(std::string{key});
if (it == values.end()) {
return {};
}
try {
return it->second.get<std::string>();
} catch (const sdbus::Error&) {
return {};
}
}
}
std::vector<std::string> get_string_array_from_variant(const std::map<std::string, sdbus::Variant>& values,
std::string_view key) {
const auto it = values.find(std::string{key});
if (it == values.end()) {
return {};
}
std::string get_object_path_from_variant(const std::map<std::string, sdbus::Variant>& values, std::string_view key) {
const auto it = values.find(std::string{key});
if (it == values.end()) {
return {};
}
try {
return it->second.get<std::vector<std::string>>();
} catch (const sdbus::Error&) {
return {};
}
}
int64_t get_int64_from_variant(const std::map<std::string, sdbus::Variant>& values, std::string_view key) {
const auto it = values.find(std::string{key});
if (it == values.end()) {
return 0;
}
try {
return it->second.get<int64_t>();
} catch (const sdbus::Error&) {
try {
const auto value = it->second.get<uint64_t>();
return value > static_cast<uint64_t>(std::numeric_limits<int64_t>::max())
? std::numeric_limits<int64_t>::max()
: static_cast<int64_t>(value);
return it->second.get<sdbus::ObjectPath>();
} catch (const sdbus::Error&) {
try {
return static_cast<int64_t>(it->second.get<int32_t>());
return it->second.get<std::string>();
} catch (const sdbus::Error&) {
return {};
}
}
}
std::vector<std::string> get_string_array_from_variant(const std::map<std::string, sdbus::Variant>& values,
std::string_view key) {
const auto it = values.find(std::string{key});
if (it == values.end()) {
return {};
}
try {
return it->second.get<std::vector<std::string>>();
} catch (const sdbus::Error&) {
return {};
}
}
int64_t get_int64_from_variant(const std::map<std::string, sdbus::Variant>& values, std::string_view key) {
const auto it = values.find(std::string{key});
if (it == values.end()) {
return 0;
}
try {
return it->second.get<int64_t>();
} catch (const sdbus::Error&) {
try {
const auto value = it->second.get<uint64_t>();
return value > static_cast<uint64_t>(std::numeric_limits<int64_t>::max()) ? std::numeric_limits<int64_t>::max()
: static_cast<int64_t>(value);
} catch (const sdbus::Error&) {
try {
return static_cast<int64_t>(it->second.get<uint32_t>());
return static_cast<int64_t>(it->second.get<int32_t>());
} catch (const sdbus::Error&) {
return 0;
try {
return static_cast<int64_t>(it->second.get<uint32_t>());
} catch (const sdbus::Error&) {
return 0;
}
}
}
}
}
}
[[maybe_unused]] std::string primary_artist(const std::vector<std::string>& artists) {
if (artists.empty()) {
return {};
}
return artists.front();
}
[[maybe_unused]] std::string joinKeys(const std::map<std::string, sdbus::Variant>& values) {
std::string out;
bool first = true;
for (const auto& [key, _] : values) {
if (!first) {
out += ", ";
[[maybe_unused]] std::string primary_artist(const std::vector<std::string>& artists) {
if (artists.empty()) {
return {};
}
first = false;
out += key;
return artists.front();
}
return out;
}
[[maybe_unused]] std::string joinStrings(const std::vector<std::string>& values) {
std::string out;
bool first = true;
for (const auto& value : values) {
if (!first) {
out += ", ";
[[maybe_unused]] std::string joinKeys(const std::map<std::string, sdbus::Variant>& values) {
std::string out;
bool first = true;
for (const auto& [key, _] : values) {
if (!first) {
out += ", ";
}
first = false;
out += key;
}
first = false;
out += value;
return out;
}
return out;
}
bool hasStrongNowPlayingMetadata(const MprisPlayerInfo& info) {
// Track IDs/source URLs can exist during transient "loading" states where the
// user-visible metadata is still placeholder-only (e.g. app identity + logo).
// Treat metadata as strong only when actual now-playing fields are present.
return !info.title.empty() || !info.artists.empty() || !info.album.empty();
}
[[maybe_unused]] std::string joinStrings(const std::vector<std::string>& values) {
std::string out;
bool first = true;
for (const auto& value : values) {
if (!first) {
out += ", ";
}
first = false;
out += value;
}
return out;
}
std::map<std::string, sdbus::Variant> to_dbus_player(const MprisPlayerInfo& info) {
std::map<std::string, sdbus::Variant> player;
player["bus_name"] = sdbus::Variant(info.busName);
player["identity"] = sdbus::Variant(info.identity);
player["desktop_entry"] = sdbus::Variant(info.desktopEntry);
player["playback_status"] = sdbus::Variant(info.playbackStatus);
player["track_id"] = sdbus::Variant(info.trackId);
player["title"] = sdbus::Variant(info.title);
player["artists"] = sdbus::Variant(info.artists);
player["album"] = sdbus::Variant(info.album);
player["source_url"] = sdbus::Variant(info.sourceUrl);
player["art_url"] = sdbus::Variant(info.artUrl);
player["loop_status"] = sdbus::Variant(info.loopStatus);
player["shuffle"] = sdbus::Variant(info.shuffle);
player["volume"] = sdbus::Variant(info.volume);
player["position_us"] = sdbus::Variant(info.positionUs);
player["length_us"] = sdbus::Variant(info.lengthUs);
player["can_play"] = sdbus::Variant(info.canPlay);
player["can_pause"] = sdbus::Variant(info.canPause);
player["can_go_next"] = sdbus::Variant(info.canGoNext);
player["can_go_previous"] = sdbus::Variant(info.canGoPrevious);
player["can_seek"] = sdbus::Variant(info.canSeek);
return player;
}
bool hasStrongNowPlayingMetadata(const MprisPlayerInfo& info) {
// Track IDs/source URLs can exist during transient "loading" states where the
// user-visible metadata is still placeholder-only (e.g. app identity + logo).
// Treat metadata as strong only when actual now-playing fields are present.
return !info.title.empty() || !info.artists.empty() || !info.album.empty();
}
constexpr Logger kLog("mpris");
std::map<std::string, sdbus::Variant> to_dbus_player(const MprisPlayerInfo& info) {
std::map<std::string, sdbus::Variant> player;
player["bus_name"] = sdbus::Variant(info.busName);
player["identity"] = sdbus::Variant(info.identity);
player["desktop_entry"] = sdbus::Variant(info.desktopEntry);
player["playback_status"] = sdbus::Variant(info.playbackStatus);
player["track_id"] = sdbus::Variant(info.trackId);
player["title"] = sdbus::Variant(info.title);
player["artists"] = sdbus::Variant(info.artists);
player["album"] = sdbus::Variant(info.album);
player["source_url"] = sdbus::Variant(info.sourceUrl);
player["art_url"] = sdbus::Variant(info.artUrl);
player["loop_status"] = sdbus::Variant(info.loopStatus);
player["shuffle"] = sdbus::Variant(info.shuffle);
player["volume"] = sdbus::Variant(info.volume);
player["position_us"] = sdbus::Variant(info.positionUs);
player["length_us"] = sdbus::Variant(info.lengthUs);
player["can_play"] = sdbus::Variant(info.canPlay);
player["can_pause"] = sdbus::Variant(info.canPause);
player["can_go_next"] = sdbus::Variant(info.canGoNext);
player["can_go_previous"] = sdbus::Variant(info.canGoPrevious);
player["can_seek"] = sdbus::Variant(info.canSeek);
return player;
}
constexpr Logger kLog("mpris");
} // namespace
@@ -802,8 +801,8 @@ void MprisService::syncSignals(const std::optional<MprisPlayerInfo>& previousAct
m_lastEmittedActivePlayer = current_active_name;
}
if (previousActive.has_value() && current_active.has_value() &&
previousActive->busName == current_active->busName && previousActive->title != current_active->title) {
if (previousActive.has_value() && current_active.has_value() && previousActive->busName == current_active->busName &&
previousActive->title != current_active->title) {
emitTrackChanged(*current_active);
}
}
@@ -923,9 +922,9 @@ void MprisService::addOrRefreshPlayer(const std::string& busName) {
const MprisPlayerInfo info = readPlayerInfo(*proxyIt->second, busName);
const auto now = std::chrono::steady_clock::now();
// kLog.debug(
// "queried player name={} identity=\"{}\" status=\"{}\" title=\"{}\" artist=\"{}\" track_id=\"{}\" art_url=\"{}\"",
// info.busName, info.identity, info.playbackStatus, info.title, primary_artist(info.artists), info.trackId,
// info.artUrl);
// "queried player name={} identity=\"{}\" status=\"{}\" title=\"{}\" artist=\"{}\" track_id=\"{}\"
// art_url=\"{}\"", info.busName, info.identity, info.playbackStatus, info.title, primary_artist(info.artists),
// info.trackId, info.artUrl);
if (info.artUrl.empty()) {
const auto metadata = get_metadata_or(*proxyIt->second);
// kLog.debug("queried player missing art url name={} metadata_keys=[{}]", info.busName, joinKeys(metadata));
@@ -1070,7 +1069,7 @@ std::optional<std::string> MprisService::chooseActivePlayer() const {
}
}
if (mostRecentPlaying.has_value()) {
//kLog.debug("choose active player source=recent_playing name={}", *mostRecentPlaying);
// kLog.debug("choose active player source=recent_playing name={}", *mostRecentPlaying);
return mostRecentPlaying;
}
+2 -2
View File
@@ -11,8 +11,8 @@
#include <vector>
namespace sdbus {
class IObject;
class IProxy;
class IObject;
class IProxy;
} // namespace sdbus
class SessionBus;
+31 -29
View File
@@ -17,41 +17,41 @@
namespace {
constexpr Logger kLog("network");
constexpr Logger kLog("network");
using SecretsDict = std::map<std::string, std::map<std::string, sdbus::Variant>>;
using SecretsDict = std::map<std::string, std::map<std::string, sdbus::Variant>>;
const sdbus::ServiceName k_agentBusName{"org.noctalia.SecretAgent"};
const sdbus::ObjectPath k_agentObjectPath{"/org/freedesktop/NetworkManager/SecretAgent"};
constexpr auto k_agentInterface = "org.freedesktop.NetworkManager.SecretAgent";
constexpr auto k_agentIdentifier = "org.noctalia.SecretAgent";
const sdbus::ServiceName k_agentBusName{"org.noctalia.SecretAgent"};
const sdbus::ObjectPath k_agentObjectPath{"/org/freedesktop/NetworkManager/SecretAgent"};
constexpr auto k_agentInterface = "org.freedesktop.NetworkManager.SecretAgent";
constexpr auto k_agentIdentifier = "org.noctalia.SecretAgent";
const sdbus::ServiceName k_nmBusName{"org.freedesktop.NetworkManager"};
const sdbus::ObjectPath k_agentManagerObjectPath{"/org/freedesktop/NetworkManager/AgentManager"};
constexpr auto k_agentManagerInterface = "org.freedesktop.NetworkManager.AgentManager";
const sdbus::ServiceName k_nmBusName{"org.freedesktop.NetworkManager"};
const sdbus::ObjectPath k_agentManagerObjectPath{"/org/freedesktop/NetworkManager/AgentManager"};
constexpr auto k_agentManagerInterface = "org.freedesktop.NetworkManager.AgentManager";
constexpr std::uint32_t k_nmSecretAgentGetSecretsFlagAllowInteraction = 0x1;
constexpr std::uint32_t k_nmSecretAgentGetSecretsFlagAllowInteraction = 0x1;
constexpr auto k_wirelessSettingName = "802-11-wireless";
constexpr auto k_wirelessSecuritySettingName = "802-11-wireless-security";
constexpr auto k_pskKey = "psk";
constexpr auto k_wirelessSettingName = "802-11-wireless";
constexpr auto k_wirelessSecuritySettingName = "802-11-wireless-security";
constexpr auto k_pskKey = "psk";
std::string extractSsid(const SecretsDict& connection) {
auto wifiIt = connection.find(k_wirelessSettingName);
if (wifiIt == connection.end()) {
return {};
std::string extractSsid(const SecretsDict& connection) {
auto wifiIt = connection.find(k_wirelessSettingName);
if (wifiIt == connection.end()) {
return {};
}
auto ssidIt = wifiIt->second.find("ssid");
if (ssidIt == wifiIt->second.end()) {
return {};
}
try {
const auto bytes = ssidIt->second.get<std::vector<std::uint8_t>>();
return std::string(bytes.begin(), bytes.end());
} catch (const sdbus::Error&) {
return {};
}
}
auto ssidIt = wifiIt->second.find("ssid");
if (ssidIt == wifiIt->second.end()) {
return {};
}
try {
const auto bytes = ssidIt->second.get<std::vector<std::uint8_t>>();
return std::string(bytes.begin(), bytes.end());
} catch (const sdbus::Error&) {
return {};
}
}
} // namespace
@@ -154,7 +154,9 @@ NetworkSecretAgent::NetworkSecretAgent(SystemBus& bus) : m_impl(std::make_unique
// Register with NM's agent manager.
try {
auto agentManager = sdbus::createProxy(bus.connection(), k_nmBusName, k_agentManagerObjectPath);
agentManager->callMethod("Register").onInterface(k_agentManagerInterface).withArguments(std::string(k_agentIdentifier));
agentManager->callMethod("Register")
.onInterface(k_agentManagerInterface)
.withArguments(std::string(k_agentIdentifier));
kLog.info("registered NetworkManager secret agent as {}", k_agentIdentifier);
} catch (const sdbus::Error& e) {
kLog.warn("secret agent registration failed: {}", e.what());
+1 -1
View File
@@ -7,7 +7,7 @@
class SystemBus;
namespace sdbus {
class IObject;
class IObject;
}
// NetworkManager secret agent. Registers with org.freedesktop.NetworkManager.AgentManager
+76 -79
View File
@@ -13,88 +13,87 @@
namespace {
constexpr Logger kLog("network");
constexpr Logger kLog("network");
const sdbus::ServiceName k_nmBusName{"org.freedesktop.NetworkManager"};
const sdbus::ObjectPath k_nmObjectPath{"/org/freedesktop/NetworkManager"};
constexpr auto k_nmInterface = "org.freedesktop.NetworkManager";
constexpr auto k_nmDeviceInterface = "org.freedesktop.NetworkManager.Device";
constexpr auto k_nmDeviceWirelessInterface = "org.freedesktop.NetworkManager.Device.Wireless";
constexpr auto k_nmSettingsInterface = "org.freedesktop.NetworkManager.Settings";
const sdbus::ObjectPath k_nmSettingsObjectPath{"/org/freedesktop/NetworkManager/Settings"};
constexpr auto k_nmSettingsConnectionInterface = "org.freedesktop.NetworkManager.Settings.Connection";
const sdbus::ServiceName k_nmBusName{"org.freedesktop.NetworkManager"};
const sdbus::ObjectPath k_nmObjectPath{"/org/freedesktop/NetworkManager"};
constexpr auto k_nmInterface = "org.freedesktop.NetworkManager";
constexpr auto k_nmDeviceInterface = "org.freedesktop.NetworkManager.Device";
constexpr auto k_nmDeviceWirelessInterface = "org.freedesktop.NetworkManager.Device.Wireless";
constexpr auto k_nmSettingsInterface = "org.freedesktop.NetworkManager.Settings";
const sdbus::ObjectPath k_nmSettingsObjectPath{"/org/freedesktop/NetworkManager/Settings"};
constexpr auto k_nmSettingsConnectionInterface = "org.freedesktop.NetworkManager.Settings.Connection";
// NM80211ApSecurityFlags bits we care about.
constexpr std::uint32_t k_nm80211ApSecNone = 0x0;
constexpr auto k_nmActiveConnectionInterface = "org.freedesktop.NetworkManager.Connection.Active";
constexpr auto k_nmAccessPointInterface = "org.freedesktop.NetworkManager.AccessPoint";
constexpr auto k_nmIp4ConfigInterface = "org.freedesktop.NetworkManager.IP4Config";
constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
// NM80211ApSecurityFlags bits we care about.
constexpr std::uint32_t k_nm80211ApSecNone = 0x0;
constexpr auto k_nmActiveConnectionInterface = "org.freedesktop.NetworkManager.Connection.Active";
constexpr auto k_nmAccessPointInterface = "org.freedesktop.NetworkManager.AccessPoint";
constexpr auto k_nmIp4ConfigInterface = "org.freedesktop.NetworkManager.IP4Config";
constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
// NMDeviceType values from NetworkManager D-Bus API.
constexpr std::uint32_t k_nmDeviceTypeWifi = 2;
// NMDeviceType values from NetworkManager D-Bus API.
constexpr std::uint32_t k_nmDeviceTypeWifi = 2;
// NMActiveConnectionState
constexpr std::uint32_t k_nmActiveConnectionStateActivated = 2;
// NMActiveConnectionState
constexpr std::uint32_t k_nmActiveConnectionStateActivated = 2;
template <typename T>
T getPropertyOr(sdbus::IProxy& proxy, std::string_view interfaceName, std::string_view propertyName, T fallback) {
try {
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(std::string(interfaceName));
return value.get<T>();
} catch (const sdbus::Error&) {
return fallback;
}
}
std::string ipv4FromUint(std::uint32_t addrLe) {
// NM stores IPv4 addresses as native-byte-order uint32 in network order bytes.
// I.e. the bytes a.b.c.d are laid out in memory low->high as a,b,c,d.
std::array<std::uint8_t, 4> bytes{};
bytes[0] = static_cast<std::uint8_t>(addrLe & 0xffU);
bytes[1] = static_cast<std::uint8_t>((addrLe >> 8) & 0xffU);
bytes[2] = static_cast<std::uint8_t>((addrLe >> 16) & 0xffU);
bytes[3] = static_cast<std::uint8_t>((addrLe >> 24) & 0xffU);
char buf[32];
std::snprintf(buf, sizeof(buf), "%u.%u.%u.%u", bytes[0], bytes[1], bytes[2], bytes[3]);
return std::string(buf);
}
std::string firstIpv4FromConfig(sdbus::IConnection& conn, const std::string& ip4ConfigPath) {
if (ip4ConfigPath.empty() || ip4ConfigPath == "/") {
return {};
}
try {
auto proxy =
sdbus::createProxy(conn, k_nmBusName, sdbus::ObjectPath{ip4ConfigPath});
// Prefer "AddressData" (vector<dict<string,variant>>) since "Addresses" is deprecated.
template <typename T>
T getPropertyOr(sdbus::IProxy& proxy, std::string_view interfaceName, std::string_view propertyName, T fallback) {
try {
const sdbus::Variant value = proxy->getProperty("AddressData").onInterface(k_nmIp4ConfigInterface);
const auto data = value.get<std::vector<std::map<std::string, sdbus::Variant>>>();
for (const auto& entry : data) {
auto it = entry.find("address");
if (it != entry.end()) {
try {
return it->second.get<std::string>();
} catch (const sdbus::Error&) {
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(std::string(interfaceName));
return value.get<T>();
} catch (const sdbus::Error&) {
return fallback;
}
}
std::string ipv4FromUint(std::uint32_t addrLe) {
// NM stores IPv4 addresses as native-byte-order uint32 in network order bytes.
// I.e. the bytes a.b.c.d are laid out in memory low->high as a,b,c,d.
std::array<std::uint8_t, 4> bytes{};
bytes[0] = static_cast<std::uint8_t>(addrLe & 0xffU);
bytes[1] = static_cast<std::uint8_t>((addrLe >> 8) & 0xffU);
bytes[2] = static_cast<std::uint8_t>((addrLe >> 16) & 0xffU);
bytes[3] = static_cast<std::uint8_t>((addrLe >> 24) & 0xffU);
char buf[32];
std::snprintf(buf, sizeof(buf), "%u.%u.%u.%u", bytes[0], bytes[1], bytes[2], bytes[3]);
return std::string(buf);
}
std::string firstIpv4FromConfig(sdbus::IConnection& conn, const std::string& ip4ConfigPath) {
if (ip4ConfigPath.empty() || ip4ConfigPath == "/") {
return {};
}
try {
auto proxy = sdbus::createProxy(conn, k_nmBusName, sdbus::ObjectPath{ip4ConfigPath});
// Prefer "AddressData" (vector<dict<string,variant>>) since "Addresses" is deprecated.
try {
const sdbus::Variant value = proxy->getProperty("AddressData").onInterface(k_nmIp4ConfigInterface);
const auto data = value.get<std::vector<std::map<std::string, sdbus::Variant>>>();
for (const auto& entry : data) {
auto it = entry.find("address");
if (it != entry.end()) {
try {
return it->second.get<std::string>();
} catch (const sdbus::Error&) {
}
}
}
} catch (const sdbus::Error&) {
}
// Fallback: legacy Addresses (vector<vector<uint32>> — addr, prefix, gateway).
try {
const sdbus::Variant value = proxy->getProperty("Addresses").onInterface(k_nmIp4ConfigInterface);
const auto data = value.get<std::vector<std::vector<std::uint32_t>>>();
if (!data.empty() && !data.front().empty()) {
return ipv4FromUint(data.front().front());
}
} catch (const sdbus::Error&) {
}
} catch (const sdbus::Error&) {
}
// Fallback: legacy Addresses (vector<vector<uint32>> — addr, prefix, gateway).
try {
const sdbus::Variant value = proxy->getProperty("Addresses").onInterface(k_nmIp4ConfigInterface);
const auto data = value.get<std::vector<std::vector<std::uint32_t>>>();
if (!data.empty() && !data.front().empty()) {
return ipv4FromUint(data.front().front());
}
} catch (const sdbus::Error&) {
}
} catch (const sdbus::Error&) {
return {};
}
return {};
}
} // namespace
@@ -137,8 +136,8 @@ NetworkService::NetworkService(SystemBus& bus) : m_bus(bus) {
if (deviceType != k_nmDeviceTypeWifi) {
continue;
}
const auto lastScan = getPropertyOr<std::int64_t>(*device, k_nmDeviceWirelessInterface, "LastScan",
std::int64_t{0});
const auto lastScan =
getPropertyOr<std::int64_t>(*device, k_nmDeviceWirelessInterface, "LastScan", std::int64_t{0});
if (lastScan > baseline) {
baseline = lastScan;
}
@@ -442,12 +441,9 @@ void NetworkService::refreshAccessPoints() {
info.ssid.assign(ssidBytes.begin(), ssidBytes.end());
} catch (const sdbus::Error&) {
}
info.strength =
getPropertyOr<std::uint8_t>(*ap, k_nmAccessPointInterface, "Strength", std::uint8_t{0});
const auto wpaFlags =
getPropertyOr<std::uint32_t>(*ap, k_nmAccessPointInterface, "WpaFlags", 0U);
const auto rsnFlags =
getPropertyOr<std::uint32_t>(*ap, k_nmAccessPointInterface, "RsnFlags", 0U);
info.strength = getPropertyOr<std::uint8_t>(*ap, k_nmAccessPointInterface, "Strength", std::uint8_t{0});
const auto wpaFlags = getPropertyOr<std::uint32_t>(*ap, k_nmAccessPointInterface, "WpaFlags", 0U);
const auto rsnFlags = getPropertyOr<std::uint32_t>(*ap, k_nmAccessPointInterface, "RsnFlags", 0U);
info.secured = (wpaFlags != k_nm80211ApSecNone) || (rsnFlags != k_nm80211ApSecNone);
if (info.ssid.empty()) {
continue; // skip hidden networks for now
@@ -531,7 +527,8 @@ void NetworkService::rebindActiveConnection() {
std::string newDevicePath;
if (m_activeConnection != nullptr) {
try {
const sdbus::Variant value = m_activeConnection->getProperty("Devices").onInterface(k_nmActiveConnectionInterface);
const sdbus::Variant value =
m_activeConnection->getProperty("Devices").onInterface(k_nmActiveConnectionInterface);
const auto devices = value.get<std::vector<sdbus::ObjectPath>>();
if (!devices.empty()) {
newDevicePath = devices.front();
+1 -1
View File
@@ -10,7 +10,7 @@
class SystemBus;
namespace sdbus {
class IProxy;
class IProxy;
}
struct AccessPointInfo {
+81 -84
View File
@@ -17,9 +17,8 @@ static const sdbus::ObjectPath k_object_path{"/org/freedesktop/Notifications"};
static constexpr auto k_interface = "org.freedesktop.Notifications";
NotificationService::NotificationService(SessionBus& bus, NotificationManager& manager) : m_manager(manager) {
m_manager.setActionInvokeCallback([this](uint32_t id, const std::string& actionKey) {
emitActionInvoked(id, actionKey);
});
m_manager.setActionInvokeCallback(
[this](uint32_t id, const std::string& actionKey) { emitActionInvoked(id, actionKey); });
bus.connection().requestName(k_bus_name);
m_object = sdbus::createObject(bus.connection(), k_object_path);
@@ -75,98 +74,96 @@ static constexpr int32_t k_min_timeout = -1;
namespace {
std::string clamp_str(std::string_view s) {
const auto len = std::min(s.size(), k_max_string_len);
return std::string{s.substr(0, len)};
}
bool isBlankText(std::string_view text) {
return text.empty() ||
std::all_of(text.begin(), text.end(),
[](unsigned char ch) { return std::isspace(ch) != 0; });
}
std::vector<std::string> sanitize_actions(const std::vector<std::string>& actions) {
static constexpr std::string_view kFallbackActionLabel = "Action";
std::vector<std::string> sanitized;
sanitized.reserve(actions.size() - (actions.size() % 2));
for (size_t i = 0; i + 1 < actions.size(); i += 2) {
std::string actionKey = clamp_str(actions[i]);
std::string label = clamp_str(actions[i + 1]);
if (actionKey.empty()) {
continue;
}
if (isBlankText(label)) {
label = kFallbackActionLabel;
}
sanitized.push_back(std::move(actionKey));
sanitized.push_back(std::move(label));
std::string clamp_str(std::string_view s) {
const auto len = std::min(s.size(), k_max_string_len);
return std::string{s.substr(0, len)};
}
return sanitized;
}
using NotificationImageDataStruct =
sdbus::Struct<std::int32_t, std::int32_t, std::int32_t, bool, std::int32_t, std::int32_t,
std::vector<std::uint8_t>>;
std::optional<NotificationImageData> decode_image_data_variant(const sdbus::Variant& value) {
try {
const auto data = value.get<NotificationImageDataStruct>();
NotificationImageData out;
out.width = std::get<0>(data);
out.height = std::get<1>(data);
out.rowStride = std::get<2>(data);
out.hasAlpha = std::get<3>(data);
out.bitsPerSample = std::get<4>(data);
out.channels = std::get<5>(data);
out.data = std::get<6>(data);
return out;
} catch (const sdbus::Error&) {
bool isBlankText(std::string_view text) {
return text.empty() ||
std::all_of(text.begin(), text.end(), [](unsigned char ch) { return std::isspace(ch) != 0; });
}
try {
const auto data = value.get<std::tuple<std::int32_t, std::int32_t, std::int32_t, bool, std::int32_t,
std::int32_t, std::vector<std::uint8_t>>>();
NotificationImageData out;
out.width = std::get<0>(data);
out.height = std::get<1>(data);
out.rowStride = std::get<2>(data);
out.hasAlpha = std::get<3>(data);
out.bitsPerSample = std::get<4>(data);
out.channels = std::get<5>(data);
out.data = std::get<6>(data);
return out;
} catch (const sdbus::Error&) {
}
std::vector<std::string> sanitize_actions(const std::vector<std::string>& actions) {
static constexpr std::string_view kFallbackActionLabel = "Action";
return std::nullopt;
}
std::vector<std::string> sanitized;
sanitized.reserve(actions.size() - (actions.size() % 2));
std::optional<NotificationImageData> decode_image_hint(const std::map<std::string, sdbus::Variant>& hints,
std::string* outSourceKey = nullptr) {
for (const char* key : {"image-data", "image_data", "icon_data"}) {
const auto it = hints.find(key);
if (it == hints.end()) {
continue;
}
for (size_t i = 0; i + 1 < actions.size(); i += 2) {
std::string actionKey = clamp_str(actions[i]);
std::string label = clamp_str(actions[i + 1]);
auto decoded = decode_image_data_variant(it->second);
if (decoded.has_value()) {
if (outSourceKey != nullptr) {
*outSourceKey = key;
if (actionKey.empty()) {
continue;
}
return decoded;
if (isBlankText(label)) {
label = kFallbackActionLabel;
}
sanitized.push_back(std::move(actionKey));
sanitized.push_back(std::move(label));
}
return sanitized;
}
return std::nullopt;
}
using NotificationImageDataStruct = sdbus::Struct<std::int32_t, std::int32_t, std::int32_t, bool, std::int32_t,
std::int32_t, std::vector<std::uint8_t>>;
std::optional<NotificationImageData> decode_image_data_variant(const sdbus::Variant& value) {
try {
const auto data = value.get<NotificationImageDataStruct>();
NotificationImageData out;
out.width = std::get<0>(data);
out.height = std::get<1>(data);
out.rowStride = std::get<2>(data);
out.hasAlpha = std::get<3>(data);
out.bitsPerSample = std::get<4>(data);
out.channels = std::get<5>(data);
out.data = std::get<6>(data);
return out;
} catch (const sdbus::Error&) {
}
try {
const auto data = value.get<std::tuple<std::int32_t, std::int32_t, std::int32_t, bool, std::int32_t, std::int32_t,
std::vector<std::uint8_t>>>();
NotificationImageData out;
out.width = std::get<0>(data);
out.height = std::get<1>(data);
out.rowStride = std::get<2>(data);
out.hasAlpha = std::get<3>(data);
out.bitsPerSample = std::get<4>(data);
out.channels = std::get<5>(data);
out.data = std::get<6>(data);
return out;
} catch (const sdbus::Error&) {
}
return std::nullopt;
}
std::optional<NotificationImageData> decode_image_hint(const std::map<std::string, sdbus::Variant>& hints,
std::string* outSourceKey = nullptr) {
for (const char* key : {"image-data", "image_data", "icon_data"}) {
const auto it = hints.find(key);
if (it == hints.end()) {
continue;
}
auto decoded = decode_image_data_variant(it->second);
if (decoded.has_value()) {
if (outSourceKey != nullptr) {
*outSourceKey = key;
}
return decoded;
}
}
return std::nullopt;
}
} // namespace
+277 -279
View File
@@ -18,8 +18,8 @@
#include <sdbus-c++/MethodResult.h>
#include <sdbus-c++/Types.h>
#include <sdbus-c++/VTableItems.h>
#include <string_view>
#include <sstream>
#include <string_view>
#include <sys/types.h>
#include <sys/wait.h>
#include <tuple>
@@ -28,249 +28,250 @@
namespace {
constexpr Logger kLog("polkit");
constexpr Logger kLog("polkit");
const sdbus::ServiceName k_authorityBusName{"org.freedesktop.PolicyKit1"};
const sdbus::ObjectPath k_authorityObjectPath{"/org/freedesktop/PolicyKit1/Authority"};
constexpr auto k_authorityInterface = "org.freedesktop.PolicyKit1.Authority";
const sdbus::ServiceName k_logindBusName{"org.freedesktop.login1"};
const sdbus::ObjectPath k_logindObjectPath{"/org/freedesktop/login1"};
constexpr auto k_logindManagerInterface = "org.freedesktop.login1.Manager";
constexpr auto k_logindSessionInterface = "org.freedesktop.login1.Session";
const sdbus::ServiceName k_authorityBusName{"org.freedesktop.PolicyKit1"};
const sdbus::ObjectPath k_authorityObjectPath{"/org/freedesktop/PolicyKit1/Authority"};
constexpr auto k_authorityInterface = "org.freedesktop.PolicyKit1.Authority";
const sdbus::ServiceName k_logindBusName{"org.freedesktop.login1"};
const sdbus::ObjectPath k_logindObjectPath{"/org/freedesktop/login1"};
constexpr auto k_logindManagerInterface = "org.freedesktop.login1.Manager";
constexpr auto k_logindSessionInterface = "org.freedesktop.login1.Session";
const sdbus::ObjectPath k_agentObjectPath{"/org/noctalia/PolkitAuthenticationAgent"};
constexpr auto k_agentInterface = "org.freedesktop.PolicyKit1.AuthenticationAgent";
const sdbus::ObjectPath k_agentObjectPath{"/org/noctalia/PolkitAuthenticationAgent"};
constexpr auto k_agentInterface = "org.freedesktop.PolicyKit1.AuthenticationAgent";
using Subject = sdbus::Struct<std::string, std::map<std::string, sdbus::Variant>>;
using Identity = sdbus::Struct<std::string, std::map<std::string, sdbus::Variant>>;
using IdentityDetails = std::map<std::string, sdbus::Variant>;
using Subject = sdbus::Struct<std::string, std::map<std::string, sdbus::Variant>>;
using Identity = sdbus::Struct<std::string, std::map<std::string, sdbus::Variant>>;
using IdentityDetails = std::map<std::string, sdbus::Variant>;
std::string localeFromEnvironment() {
if (const char* lang = std::getenv("LANG"); lang != nullptr && lang[0] != '\0') {
return lang;
}
return "C";
}
std::optional<std::string> usernameFromUid(std::uint32_t uid) {
passwd pwd{};
passwd* result = nullptr;
std::array<char, 4096> buffer{};
const int rc = getpwuid_r(static_cast<uid_t>(uid), &pwd, buffer.data(), buffer.size(), &result);
if (rc != 0 || result == nullptr || result->pw_name == nullptr || result->pw_name[0] == '\0') {
return std::nullopt;
}
return std::string(result->pw_name);
}
std::optional<std::string> identityUsername(const Identity& identity) {
const std::string& kind = std::get<0>(identity);
const IdentityDetails& details = std::get<1>(identity);
if (kind != "unix-user") {
return std::nullopt;
}
const auto nameIt = details.find("name");
if (nameIt != details.end()) {
try {
const std::string name = nameIt->second.get<std::string>();
if (!name.empty()) {
return name;
}
} catch (const sdbus::Error&) {
std::string localeFromEnvironment() {
if (const char* lang = std::getenv("LANG"); lang != nullptr && lang[0] != '\0') {
return lang;
}
return "C";
}
const auto uidIt = details.find("uid");
if (uidIt != details.end()) {
try {
return usernameFromUid(uidIt->second.get<std::uint32_t>());
} catch (const sdbus::Error&) {
}
}
return std::nullopt;
}
bool writeAll(int fd, std::string_view payload) {
const char* cursor = payload.data();
std::size_t remaining = payload.size();
while (remaining > 0) {
const ssize_t written = ::write(fd, cursor, remaining);
if (written < 0) {
if (errno == EINTR) {
continue;
}
return false;
}
cursor += static_cast<std::size_t>(written);
remaining -= static_cast<std::size_t>(written);
}
return true;
}
std::optional<std::string> readLineNonBlocking(int fd, std::string& buffer) {
char chunk[256];
while (true) {
const ssize_t n = ::read(fd, chunk, sizeof(chunk));
if (n < 0) {
if (errno == EINTR) {
continue;
}
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
}
std::optional<std::string> usernameFromUid(std::uint32_t uid) {
passwd pwd{};
passwd* result = nullptr;
std::array<char, 4096> buffer{};
const int rc = getpwuid_r(static_cast<uid_t>(uid), &pwd, buffer.data(), buffer.size(), &result);
if (rc != 0 || result == nullptr || result->pw_name == nullptr || result->pw_name[0] == '\0') {
return std::nullopt;
}
if (n == 0) {
if (buffer.empty()) {
return std::string(result->pw_name);
}
std::optional<std::string> identityUsername(const Identity& identity) {
const std::string& kind = std::get<0>(identity);
const IdentityDetails& details = std::get<1>(identity);
if (kind != "unix-user") {
return std::nullopt;
}
const auto nameIt = details.find("name");
if (nameIt != details.end()) {
try {
const std::string name = nameIt->second.get<std::string>();
if (!name.empty()) {
return name;
}
} catch (const sdbus::Error&) {
}
}
const auto uidIt = details.find("uid");
if (uidIt != details.end()) {
try {
return usernameFromUid(uidIt->second.get<std::uint32_t>());
} catch (const sdbus::Error&) {
}
}
return std::nullopt;
}
bool writeAll(int fd, std::string_view payload) {
const char* cursor = payload.data();
std::size_t remaining = payload.size();
while (remaining > 0) {
const ssize_t written = ::write(fd, cursor, remaining);
if (written < 0) {
if (errno == EINTR) {
continue;
}
return false;
}
cursor += static_cast<std::size_t>(written);
remaining -= static_cast<std::size_t>(written);
}
return true;
}
std::optional<std::string> readLineNonBlocking(int fd, std::string& buffer) {
char chunk[256];
while (true) {
const ssize_t n = ::read(fd, chunk, sizeof(chunk));
if (n < 0) {
if (errno == EINTR) {
continue;
}
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
}
return std::nullopt;
}
std::string out = std::move(buffer);
buffer.clear();
return out;
}
buffer.append(chunk, static_cast<std::size_t>(n));
const std::size_t newlinePos = buffer.find('\n');
if (newlinePos != std::string::npos) {
std::string line = buffer.substr(0, newlinePos);
buffer.erase(0, newlinePos + 1);
return line;
}
}
return std::string{};
}
bool setFdNonBlocking(int fd) {
const int flags = ::fcntl(fd, F_GETFL, 0);
if (flags < 0) {
return false;
}
return ::fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
}
std::optional<std::string> helperPath() {
constexpr std::array<const char*, 2> candidates = {
"/usr/lib/polkit-1/polkit-agent-helper-1",
"/usr/libexec/polkit-agent-helper-1",
};
for (const char* path : candidates) {
if (::access(path, X_OK) == 0) {
return std::string(path);
}
}
return std::nullopt;
}
std::string extractPromptText(const std::string& line, const std::string& token) {
if (line.size() <= token.size()) {
return "Authentication required";
}
std::string prompt = line.substr(token.size());
if (!prompt.empty() && prompt.front() == ' ') {
prompt.erase(prompt.begin());
}
return prompt;
}
std::optional<std::string> sessionIdFromLogind(SystemBus& bus) {
try {
auto manager = sdbus::createProxy(bus.connection(), k_logindBusName, k_logindObjectPath);
sdbus::ObjectPath sessionPath;
manager->callMethod("GetSessionByPID")
.onInterface(k_logindManagerInterface)
.withArguments(static_cast<std::uint32_t>(::getpid()))
.storeResultsTo(sessionPath);
auto session = sdbus::createProxy(bus.connection(), k_logindBusName, sessionPath);
const sdbus::Variant idVar = session->getProperty("Id").onInterface(k_logindSessionInterface);
const std::string sessionId = idVar.get<std::string>();
if (!sessionId.empty()) {
return sessionId;
}
} catch (const sdbus::Error&) {
}
return std::nullopt;
}
Subject makeSessionSubject(SystemBus& bus) {
if (const auto sessionId = sessionIdFromLogind(bus); sessionId.has_value()) {
std::map<std::string, sdbus::Variant> sessionDetails;
sessionDetails.emplace("session-id", sdbus::Variant{*sessionId});
return Subject{"unix-session", std::move(sessionDetails)};
}
if (const char* sessionId = std::getenv("XDG_SESSION_ID"); sessionId != nullptr && sessionId[0] != '\0') {
std::map<std::string, sdbus::Variant> sessionDetails;
sessionDetails.emplace("session-id", sdbus::Variant{std::string(sessionId)});
return Subject{"unix-session", std::move(sessionDetails)};
}
// Fallback when the session id is unavailable: register for this process.
std::map<std::string, sdbus::Variant> details;
const pid_t pid = ::getpid();
std::ifstream statFile("/proc/self/stat");
if (statFile.is_open()) {
std::string line;
std::getline(statFile, line);
const std::size_t rightParen = line.rfind(')');
if (rightParen != std::string::npos && rightParen + 2 < line.size()) {
// /proc/<pid>/stat field 22 is process start time (clock ticks since boot).
std::istringstream rest(line.substr(rightParen + 2));
std::string field;
std::uint64_t startTime = 0;
for (int fieldIndex = 3; fieldIndex <= 22; ++fieldIndex) {
if (!(rest >> field)) {
break;
}
if (fieldIndex == 22) {
try {
startTime = static_cast<std::uint64_t>(std::stoull(field));
} catch (const std::exception&) {
startTime = 0;
}
break;
if (n == 0) {
if (buffer.empty()) {
return std::nullopt;
}
std::string out = std::move(buffer);
buffer.clear();
return out;
}
buffer.append(chunk, static_cast<std::size_t>(n));
const std::size_t newlinePos = buffer.find('\n');
if (newlinePos != std::string::npos) {
std::string line = buffer.substr(0, newlinePos);
buffer.erase(0, newlinePos + 1);
return line;
}
details.emplace("pid", sdbus::Variant{static_cast<std::uint32_t>(pid)});
details.emplace("start-time", sdbus::Variant{startTime});
return Subject{"unix-process", std::move(details)};
}
return std::string{};
}
// Fallback when /proc parsing fails.
details.emplace("pid", sdbus::Variant{static_cast<std::uint32_t>(pid)});
details.emplace("start-time", sdbus::Variant{static_cast<std::uint64_t>(0)});
return Subject{"unix-process", std::move(details)};
}
PolkitIdentity toPolkitIdentity(const Identity& wireIdentity) {
PolkitIdentity identity;
identity.kind = std::get<0>(wireIdentity);
const auto& details = std::get<1>(wireIdentity);
bool setFdNonBlocking(int fd) {
const int flags = ::fcntl(fd, F_GETFL, 0);
if (flags < 0) {
return false;
}
return ::fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
}
const auto uidIt = details.find("uid");
if (uidIt != details.end()) {
std::optional<std::string> helperPath() {
constexpr std::array<const char*, 2> candidates = {
"/usr/lib/polkit-1/polkit-agent-helper-1",
"/usr/libexec/polkit-agent-helper-1",
};
for (const char* path : candidates) {
if (::access(path, X_OK) == 0) {
return std::string(path);
}
}
return std::nullopt;
}
std::string extractPromptText(const std::string& line, const std::string& token) {
if (line.size() <= token.size()) {
return "Authentication required";
}
std::string prompt = line.substr(token.size());
if (!prompt.empty() && prompt.front() == ' ') {
prompt.erase(prompt.begin());
}
return prompt;
}
std::optional<std::string> sessionIdFromLogind(SystemBus& bus) {
try {
identity.uid = uidIt->second.get<std::uint32_t>();
auto manager = sdbus::createProxy(bus.connection(), k_logindBusName, k_logindObjectPath);
sdbus::ObjectPath sessionPath;
manager->callMethod("GetSessionByPID")
.onInterface(k_logindManagerInterface)
.withArguments(static_cast<std::uint32_t>(::getpid()))
.storeResultsTo(sessionPath);
auto session = sdbus::createProxy(bus.connection(), k_logindBusName, sessionPath);
const sdbus::Variant idVar = session->getProperty("Id").onInterface(k_logindSessionInterface);
const std::string sessionId = idVar.get<std::string>();
if (!sessionId.empty()) {
return sessionId;
}
} catch (const sdbus::Error&) {
}
return std::nullopt;
}
const auto userNameIt = details.find("name");
if (userNameIt != details.end()) {
try {
identity.userName = userNameIt->second.get<std::string>();
} catch (const sdbus::Error&) {
}
}
return identity;
}
std::optional<std::size_t> firstSupportedIdentityIndex(const std::vector<Identity>& identities, std::string& outUser) {
for (std::size_t i = 0; i < identities.size(); ++i) {
if (auto resolved = identityUsername(identities[i]); resolved.has_value()) {
outUser = *resolved;
return i;
Subject makeSessionSubject(SystemBus& bus) {
if (const auto sessionId = sessionIdFromLogind(bus); sessionId.has_value()) {
std::map<std::string, sdbus::Variant> sessionDetails;
sessionDetails.emplace("session-id", sdbus::Variant{*sessionId});
return Subject{"unix-session", std::move(sessionDetails)};
}
if (const char* sessionId = std::getenv("XDG_SESSION_ID"); sessionId != nullptr && sessionId[0] != '\0') {
std::map<std::string, sdbus::Variant> sessionDetails;
sessionDetails.emplace("session-id", sdbus::Variant{std::string(sessionId)});
return Subject{"unix-session", std::move(sessionDetails)};
}
// Fallback when the session id is unavailable: register for this process.
std::map<std::string, sdbus::Variant> details;
const pid_t pid = ::getpid();
std::ifstream statFile("/proc/self/stat");
if (statFile.is_open()) {
std::string line;
std::getline(statFile, line);
const std::size_t rightParen = line.rfind(')');
if (rightParen != std::string::npos && rightParen + 2 < line.size()) {
// /proc/<pid>/stat field 22 is process start time (clock ticks since boot).
std::istringstream rest(line.substr(rightParen + 2));
std::string field;
std::uint64_t startTime = 0;
for (int fieldIndex = 3; fieldIndex <= 22; ++fieldIndex) {
if (!(rest >> field)) {
break;
}
if (fieldIndex == 22) {
try {
startTime = static_cast<std::uint64_t>(std::stoull(field));
} catch (const std::exception&) {
startTime = 0;
}
break;
}
}
details.emplace("pid", sdbus::Variant{static_cast<std::uint32_t>(pid)});
details.emplace("start-time", sdbus::Variant{startTime});
return Subject{"unix-process", std::move(details)};
}
}
// Fallback when /proc parsing fails.
details.emplace("pid", sdbus::Variant{static_cast<std::uint32_t>(pid)});
details.emplace("start-time", sdbus::Variant{static_cast<std::uint64_t>(0)});
return Subject{"unix-process", std::move(details)};
}
PolkitIdentity toPolkitIdentity(const Identity& wireIdentity) {
PolkitIdentity identity;
identity.kind = std::get<0>(wireIdentity);
const auto& details = std::get<1>(wireIdentity);
const auto uidIt = details.find("uid");
if (uidIt != details.end()) {
try {
identity.uid = uidIt->second.get<std::uint32_t>();
} catch (const sdbus::Error&) {
}
}
const auto userNameIt = details.find("name");
if (userNameIt != details.end()) {
try {
identity.userName = userNameIt->second.get<std::string>();
} catch (const sdbus::Error&) {
}
}
return identity;
}
std::optional<std::size_t> firstSupportedIdentityIndex(const std::vector<Identity>& identities,
std::string& outUser) {
for (std::size_t i = 0; i < identities.size(); ++i) {
if (auto resolved = identityUsername(identities[i]); resolved.has_value()) {
outUser = *resolved;
return i;
}
}
return std::nullopt;
}
return std::nullopt;
}
} // namespace
@@ -486,63 +487,62 @@ PolkitAgent::PolkitAgent(SystemBus& bus) : m_impl(std::make_unique<Impl>(bus)) {
m_impl->object = sdbus::createObject(bus.connection(), k_agentObjectPath);
m_impl->object
->addVTable(
sdbus::registerMethod("BeginAuthentication")
.withInputParamNames("action_id", "message", "icon_name", "details", "cookie", "identities")
.implementedAs([this](sdbus::Result<>&& result, std::string actionId, std::string message,
std::string iconName,
std::map<std::string, std::string> /*details*/, std::string cookie,
std::vector<Identity> identities) {
if (m_impl == nullptr) {
result.returnResults();
return;
}
if (m_impl->pendingBeginResult.has_value()) {
// Defensive: finish any previous deferred call before replacing state.
m_impl->finishBeginAuthenticationCall();
}
if (m_impl->pending.has_value()) {
m_impl->stopHelper();
m_impl->clearConversationState();
}
m_impl->pendingBeginResult = std::move(result);
PolkitRequest request;
request.actionId = std::move(actionId);
request.message = std::move(message);
request.iconName = std::move(iconName);
request.cookie = std::move(cookie);
request.identities.reserve(identities.size());
for (const auto& identity : identities) {
request.identities.push_back(toPolkitIdentity(identity));
}
m_impl->pending = request;
m_impl->pendingIdentitiesWire = std::move(identities);
->addVTable(sdbus::registerMethod("BeginAuthentication")
.withInputParamNames("action_id", "message", "icon_name", "details", "cookie", "identities")
.implementedAs([this](sdbus::Result<>&& result, std::string actionId, std::string message,
std::string iconName, std::map<std::string, std::string> /*details*/,
std::string cookie, std::vector<Identity> identities) {
if (m_impl == nullptr) {
result.returnResults();
return;
}
if (m_impl->pendingBeginResult.has_value()) {
// Defensive: finish any previous deferred call before replacing state.
m_impl->finishBeginAuthenticationCall();
}
if (m_impl->pending.has_value()) {
m_impl->stopHelper();
m_impl->clearConversationState();
}
m_impl->pendingBeginResult = std::move(result);
PolkitRequest request;
request.actionId = std::move(actionId);
request.message = std::move(message);
request.iconName = std::move(iconName);
request.cookie = std::move(cookie);
request.identities.reserve(identities.size());
for (const auto& identity : identities) {
request.identities.push_back(toPolkitIdentity(identity));
}
m_impl->pending = request;
m_impl->pendingIdentitiesWire = std::move(identities);
std::string userName;
const auto selectedIdentityIndex =
firstSupportedIdentityIndex(m_impl->pendingIdentitiesWire, userName);
if (!selectedIdentityIndex.has_value()) {
kLog.warn("polkit request \"{}\" has no unix-user identity", request.actionId);
m_impl->clearPending();
return;
}
m_impl->activeIdentityIndex = *selectedIdentityIndex;
m_impl->activeUser = userName;
if (!m_impl->startHelperSession()) {
kLog.warn("polkit helper startup failed for action \"{}\"", request.actionId);
m_impl->clearPending();
return;
}
}),
sdbus::registerMethod("CancelAuthentication").withInputParamNames("cookie").implementedAs(
[this](const std::string& cookie) {
if (m_impl == nullptr || !m_impl->pending.has_value()) {
return;
}
if (m_impl->pending->cookie == cookie) {
m_impl->clearPending();
}
}))
std::string userName;
const auto selectedIdentityIndex =
firstSupportedIdentityIndex(m_impl->pendingIdentitiesWire, userName);
if (!selectedIdentityIndex.has_value()) {
kLog.warn("polkit request \"{}\" has no unix-user identity", request.actionId);
m_impl->clearPending();
return;
}
m_impl->activeIdentityIndex = *selectedIdentityIndex;
m_impl->activeUser = userName;
if (!m_impl->startHelperSession()) {
kLog.warn("polkit helper startup failed for action \"{}\"", request.actionId);
m_impl->clearPending();
return;
}
}),
sdbus::registerMethod("CancelAuthentication")
.withInputParamNames("cookie")
.implementedAs([this](const std::string& cookie) {
if (m_impl == nullptr || !m_impl->pending.has_value()) {
return;
}
if (m_impl->pending->cookie == cookie) {
m_impl->clearPending();
}
}))
.forInterface(k_agentInterface);
try {
@@ -654,6 +654,4 @@ std::string PolkitAgent::supplementaryMessage() const {
return m_impl != nullptr ? m_impl->supplementaryMessage : std::string{};
}
bool PolkitAgent::supplementaryIsError() const noexcept {
return m_impl != nullptr && m_impl->supplementaryError;
}
bool PolkitAgent::supplementaryIsError() const noexcept { return m_impl != nullptr && m_impl->supplementaryError; }
+1 -1
View File
@@ -3,9 +3,9 @@
#include <cstdint>
#include <functional>
#include <memory>
#include <poll.h>
#include <string>
#include <vector>
#include <poll.h>
class SystemBus;
+37 -38
View File
@@ -23,49 +23,48 @@ std::string profileLabel(std::string_view profile) {
namespace {
constexpr Logger kLog("power");
constexpr Logger kLog("power");
static const sdbus::ServiceName k_powerProfilesBusName{"org.freedesktop.UPower.PowerProfiles"};
static const sdbus::ObjectPath k_powerProfilesObjectPath{"/org/freedesktop/UPower/PowerProfiles"};
static constexpr auto k_powerProfilesInterface = "org.freedesktop.UPower.PowerProfiles";
static constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
static const sdbus::ServiceName k_powerProfilesBusName{"org.freedesktop.UPower.PowerProfiles"};
static const sdbus::ObjectPath k_powerProfilesObjectPath{"/org/freedesktop/UPower/PowerProfiles"};
static constexpr auto k_powerProfilesInterface = "org.freedesktop.UPower.PowerProfiles";
static constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
template <typename T>
T getPropertyOr(sdbus::IProxy& proxy, std::string_view propertyName, T fallback) {
try {
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(k_powerProfilesInterface);
return value.get<T>();
} catch (const sdbus::Error&) {
return fallback;
}
}
std::vector<std::string> decodeProfiles(const sdbus::Variant& value) {
std::vector<std::string> profiles;
try {
const auto profileMaps = value.get<std::vector<std::map<std::string, sdbus::Variant>>>();
profiles.reserve(profileMaps.size());
for (const auto& profileMap : profileMaps) {
auto it = profileMap.find("Profile");
if (it == profileMap.end()) {
continue;
}
try {
const std::string profile = it->second.get<std::string>();
if (!profile.empty()) {
profiles.push_back(profile);
}
} catch (const sdbus::Error&) {
}
template <typename T> T getPropertyOr(sdbus::IProxy& proxy, std::string_view propertyName, T fallback) {
try {
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(k_powerProfilesInterface);
return value.get<T>();
} catch (const sdbus::Error&) {
return fallback;
}
} catch (const sdbus::Error&) {
}
std::ranges::sort(profiles);
profiles.erase(std::unique(profiles.begin(), profiles.end()), profiles.end());
return profiles;
}
std::vector<std::string> decodeProfiles(const sdbus::Variant& value) {
std::vector<std::string> profiles;
try {
const auto profileMaps = value.get<std::vector<std::map<std::string, sdbus::Variant>>>();
profiles.reserve(profileMaps.size());
for (const auto& profileMap : profileMaps) {
auto it = profileMap.find("Profile");
if (it == profileMap.end()) {
continue;
}
try {
const std::string profile = it->second.get<std::string>();
if (!profile.empty()) {
profiles.push_back(profile);
}
} catch (const sdbus::Error&) {
}
}
} catch (const sdbus::Error&) {
}
std::ranges::sort(profiles);
profiles.erase(std::unique(profiles.begin(), profiles.end()), profiles.end());
return profiles;
}
} // namespace
+1 -1
View File
@@ -9,7 +9,7 @@
class SystemBus;
namespace sdbus {
class IProxy;
class IProxy;
}
[[nodiscard]] std::string profileLabel(std::string_view profile);
+274 -282
View File
@@ -10,239 +10,240 @@
namespace {
static const sdbus::ServiceName k_watcher_bus_name{"org.kde.StatusNotifierWatcher"};
static const sdbus::ObjectPath k_watcher_object_path{"/StatusNotifierWatcher"};
static constexpr auto k_watcher_interface = "org.kde.StatusNotifierWatcher";
static const sdbus::ServiceName k_watcher_bus_name{"org.kde.StatusNotifierWatcher"};
static const sdbus::ObjectPath k_watcher_object_path{"/StatusNotifierWatcher"};
static constexpr auto k_watcher_interface = "org.kde.StatusNotifierWatcher";
static const sdbus::ServiceName k_dbus_name{"org.freedesktop.DBus"};
static const sdbus::ObjectPath k_dbus_path{"/org/freedesktop/DBus"};
static constexpr auto k_dbus_interface = "org.freedesktop.DBus";
static constexpr auto k_item_interface = "org.kde.StatusNotifierItem";
static constexpr auto k_menu_interface = "com.canonical.dbusmenu";
static constexpr auto k_default_item_path = "/StatusNotifierItem";
static const sdbus::ServiceName k_dbus_name{"org.freedesktop.DBus"};
static const sdbus::ObjectPath k_dbus_path{"/org/freedesktop/DBus"};
static constexpr auto k_dbus_interface = "org.freedesktop.DBus";
static constexpr auto k_item_interface = "org.kde.StatusNotifierItem";
static constexpr auto k_menu_interface = "com.canonical.dbusmenu";
static constexpr auto k_default_item_path = "/StatusNotifierItem";
bool starts_with_slash(std::string_view value) { return !value.empty() && value.front() == '/'; }
bool starts_with_slash(std::string_view value) { return !value.empty() && value.front() == '/'; }
bool looks_like_dbus_name(std::string_view value) {
return !value.empty() && value != "__path_only__";
}
bool looks_like_dbus_name(std::string_view value) { return !value.empty() && value != "__path_only__"; }
std::string lower_copy(std::string_view value) {
std::string out(value);
std::ranges::transform(out, out.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return out;
}
std::string lower_copy(std::string_view value) {
std::string out(value);
std::ranges::transform(out, out.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return out;
}
std::vector<std::string> path_name_hints(std::string_view objectPath) {
std::vector<std::string> hints;
if (objectPath.empty()) {
return hints;
}
auto push = [&hints](std::string value) {
if (value.empty()) {
return;
}
value = lower_copy(value);
if (std::ranges::find(hints, value) == hints.end()) {
hints.push_back(std::move(value));
}
};
std::string tail(objectPath);
if (const auto slash = tail.find_last_of('/'); slash != std::string::npos && slash + 1 < tail.size()) {
tail = tail.substr(slash + 1);
}
push(tail);
std::string dashed = tail;
std::replace(dashed.begin(), dashed.end(), '_', '-');
push(dashed);
std::string underscored = tail;
std::replace(underscored.begin(), underscored.end(), '-', '_');
push(underscored);
for (const auto& suffix : {"_client", "-client", ".desktop"}) {
for (const auto& candidate : std::vector<std::string>{tail, dashed, underscored}) {
if (candidate.size() > std::char_traits<char>::length(suffix) && candidate.ends_with(suffix)) {
push(candidate.substr(0, candidate.size() - std::char_traits<char>::length(suffix)));
}
}
}
std::vector<std::string> path_name_hints(std::string_view objectPath) {
std::vector<std::string> hints;
if (objectPath.empty()) {
return hints;
}
auto push = [&hints](std::string value) {
if (value.empty()) {
return;
}
value = lower_copy(value);
if (std::ranges::find(hints, value) == hints.end()) {
hints.push_back(std::move(value));
}
};
std::string tail(objectPath);
if (const auto slash = tail.find_last_of('/'); slash != std::string::npos && slash + 1 < tail.size()) {
tail = tail.substr(slash + 1);
}
push(tail);
std::string dashed = tail;
std::replace(dashed.begin(), dashed.end(), '_', '-');
push(dashed);
std::string underscored = tail;
std::replace(underscored.begin(), underscored.end(), '-', '_');
push(underscored);
for (const auto& suffix : {"_client", "-client", ".desktop"}) {
for (const auto& candidate : std::vector<std::string>{tail, dashed, underscored}) {
if (candidate.size() > std::char_traits<char>::length(suffix) && candidate.ends_with(suffix)) {
push(candidate.substr(0, candidate.size() - std::char_traits<char>::length(suffix)));
std::string stripMnemonicUnderscores(std::string label) {
std::string out;
out.reserve(label.size());
for (std::size_t i = 0; i < label.size(); ++i) {
if (label[i] == '_') {
if (i + 1 < label.size() && label[i + 1] == '_') {
out.push_back('_');
++i;
}
continue;
}
}
}
return hints;
}
std::string stripMnemonicUnderscores(std::string label) {
std::string out;
out.reserve(label.size());
for (std::size_t i = 0; i < label.size(); ++i) {
if (label[i] == '_') {
if (i + 1 < label.size() && label[i + 1] == '_') {
out.push_back('_');
++i;
}
continue;
}
out.push_back(label[i]);
}
return out;
}
template <typename T>
T get_property_or(sdbus::IProxy& proxy, std::string_view property_name, T fallback) {
try {
const sdbus::Variant value = proxy.getProperty(property_name).onInterface(k_item_interface);
return value.get<T>();
} catch (const sdbus::Error&) {
return fallback;
}
}
std::string get_item_property_string_or(sdbus::IProxy& proxy, std::string_view propertyName, std::string fallback) {
try {
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(k_item_interface);
try {
return value.get<std::string>();
} catch (const sdbus::Error&) {
}
try {
return value.get<sdbus::ObjectPath>();
} catch (const sdbus::Error&) {
}
} catch (const sdbus::Error&) {
}
return fallback;
}
using IconPixmapTuple = std::tuple<std::int32_t, std::int32_t, std::vector<std::uint8_t>>;
using IconPixmapStruct = sdbus::Struct<std::int32_t, std::int32_t, std::vector<std::uint8_t>>;
using DbusMenuLayout = sdbus::Struct<std::int32_t, std::map<std::string, sdbus::Variant>, std::vector<sdbus::Variant>>;
TrayMenuEntry decodeMenuEntry(const DbusMenuLayout& entryLayout) {
TrayMenuEntry out;
out.id = std::get<0>(entryLayout);
const auto& props = std::get<1>(entryLayout);
if (const auto it = props.find("label"); it != props.end()) {
try {
out.label = stripMnemonicUnderscores(it->second.get<std::string>());
} catch (const sdbus::Error&) {
}
}
if (const auto it = props.find("enabled"); it != props.end()) {
try {
out.enabled = it->second.get<bool>();
} catch (const sdbus::Error&) {
}
}
if (const auto it = props.find("visible"); it != props.end()) {
try {
out.visible = it->second.get<bool>();
} catch (const sdbus::Error&) {
}
}
if (const auto it = props.find("type"); it != props.end()) {
try {
out.separator = (it->second.get<std::string>() == "separator");
} catch (const sdbus::Error&) {
}
}
if (const auto it = props.find("children-display"); it != props.end()) {
try {
out.hasSubmenu = (it->second.get<std::string>() == "submenu");
} catch (const sdbus::Error&) {
}
}
return out;
}
std::vector<IconPixmapTuple> iconPixmapsFromVariant(const sdbus::Variant& value) {
try {
return value.get<std::vector<IconPixmapTuple>>();
} catch (const sdbus::Error&) {
}
try {
const auto structs = value.get<std::vector<IconPixmapStruct>>();
std::vector<IconPixmapTuple> out;
out.reserve(structs.size());
for (const auto& entry : structs) {
out.emplace_back(std::get<0>(entry), std::get<1>(entry), std::get<2>(entry));
out.push_back(label[i]);
}
return out;
} catch (const sdbus::Error&) {
}
return {};
}
std::vector<IconPixmapTuple> get_icon_pixmaps_or(sdbus::IProxy& proxy, std::string_view property_name,
const std::vector<IconPixmapTuple>& fallback) {
try {
const sdbus::Variant value = proxy.getProperty(property_name).onInterface(k_item_interface);
const auto decoded = iconPixmapsFromVariant(value);
if (!decoded.empty()) {
return decoded;
template <typename T> T get_property_or(sdbus::IProxy& proxy, std::string_view property_name, T fallback) {
try {
const sdbus::Variant value = proxy.getProperty(property_name).onInterface(k_item_interface);
return value.get<T>();
} catch (const sdbus::Error&) {
return fallback;
}
} catch (const sdbus::Error&) {
}
try {
std::map<std::string, sdbus::Variant> all;
proxy.callMethod("GetAll").onInterface("org.freedesktop.DBus.Properties").withArguments(k_item_interface).storeResultsTo(all);
const auto it = all.find(std::string(property_name));
if (it != all.end()) {
const auto decoded = iconPixmapsFromVariant(it->second);
std::string get_item_property_string_or(sdbus::IProxy& proxy, std::string_view propertyName, std::string fallback) {
try {
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(k_item_interface);
try {
return value.get<std::string>();
} catch (const sdbus::Error&) {
}
try {
return value.get<sdbus::ObjectPath>();
} catch (const sdbus::Error&) {
}
} catch (const sdbus::Error&) {
}
return fallback;
}
using IconPixmapTuple = std::tuple<std::int32_t, std::int32_t, std::vector<std::uint8_t>>;
using IconPixmapStruct = sdbus::Struct<std::int32_t, std::int32_t, std::vector<std::uint8_t>>;
using DbusMenuLayout =
sdbus::Struct<std::int32_t, std::map<std::string, sdbus::Variant>, std::vector<sdbus::Variant>>;
TrayMenuEntry decodeMenuEntry(const DbusMenuLayout& entryLayout) {
TrayMenuEntry out;
out.id = std::get<0>(entryLayout);
const auto& props = std::get<1>(entryLayout);
if (const auto it = props.find("label"); it != props.end()) {
try {
out.label = stripMnemonicUnderscores(it->second.get<std::string>());
} catch (const sdbus::Error&) {
}
}
if (const auto it = props.find("enabled"); it != props.end()) {
try {
out.enabled = it->second.get<bool>();
} catch (const sdbus::Error&) {
}
}
if (const auto it = props.find("visible"); it != props.end()) {
try {
out.visible = it->second.get<bool>();
} catch (const sdbus::Error&) {
}
}
if (const auto it = props.find("type"); it != props.end()) {
try {
out.separator = (it->second.get<std::string>() == "separator");
} catch (const sdbus::Error&) {
}
}
if (const auto it = props.find("children-display"); it != props.end()) {
try {
out.hasSubmenu = (it->second.get<std::string>() == "submenu");
} catch (const sdbus::Error&) {
}
}
return out;
}
std::vector<IconPixmapTuple> iconPixmapsFromVariant(const sdbus::Variant& value) {
try {
return value.get<std::vector<IconPixmapTuple>>();
} catch (const sdbus::Error&) {
}
try {
const auto structs = value.get<std::vector<IconPixmapStruct>>();
std::vector<IconPixmapTuple> out;
out.reserve(structs.size());
for (const auto& entry : structs) {
out.emplace_back(std::get<0>(entry), std::get<1>(entry), std::get<2>(entry));
}
return out;
} catch (const sdbus::Error&) {
}
return {};
}
std::vector<IconPixmapTuple> get_icon_pixmaps_or(sdbus::IProxy& proxy, std::string_view property_name,
const std::vector<IconPixmapTuple>& fallback) {
try {
const sdbus::Variant value = proxy.getProperty(property_name).onInterface(k_item_interface);
const auto decoded = iconPixmapsFromVariant(value);
if (!decoded.empty()) {
return decoded;
}
} catch (const sdbus::Error&) {
}
} catch (const sdbus::Error&) {
try {
std::map<std::string, sdbus::Variant> all;
proxy.callMethod("GetAll")
.onInterface("org.freedesktop.DBus.Properties")
.withArguments(k_item_interface)
.storeResultsTo(all);
const auto it = all.find(std::string(property_name));
if (it != all.end()) {
const auto decoded = iconPixmapsFromVariant(it->second);
if (!decoded.empty()) {
return decoded;
}
}
} catch (const sdbus::Error&) {
}
return fallback;
}
return fallback;
}
bool pickBestPixmap(const std::vector<IconPixmapTuple>& pixmaps, std::vector<std::uint8_t>& outArgb,
std::int32_t& outW, std::int32_t& outH) {
std::size_t bestIndex = static_cast<std::size_t>(-1);
std::int64_t bestArea = -1;
bool pickBestPixmap(const std::vector<IconPixmapTuple>& pixmaps, std::vector<std::uint8_t>& outArgb, std::int32_t& outW,
std::int32_t& outH) {
std::size_t bestIndex = static_cast<std::size_t>(-1);
std::int64_t bestArea = -1;
for (std::size_t i = 0; i < pixmaps.size(); ++i) {
const auto& [w, h, data] = pixmaps[i];
if (w <= 0 || h <= 0 || data.empty()) {
continue;
}
if (static_cast<std::size_t>(w * h * 4) > data.size()) {
continue;
}
for (std::size_t i = 0; i < pixmaps.size(); ++i) {
const auto& [w, h, data] = pixmaps[i];
if (w <= 0 || h <= 0 || data.empty()) {
continue;
}
if (static_cast<std::size_t>(w * h * 4) > data.size()) {
continue;
const std::int64_t area = static_cast<std::int64_t>(w) * static_cast<std::int64_t>(h);
if (area > bestArea) {
bestArea = area;
bestIndex = i;
}
}
const std::int64_t area = static_cast<std::int64_t>(w) * static_cast<std::int64_t>(h);
if (area > bestArea) {
bestArea = area;
bestIndex = i;
if (bestIndex == static_cast<std::size_t>(-1)) {
outArgb.clear();
outW = 0;
outH = 0;
return false;
}
const auto& [w, h, data] = pixmaps[bestIndex];
outW = w;
outH = h;
outArgb = data;
return true;
}
if (bestIndex == static_cast<std::size_t>(-1)) {
outArgb.clear();
outW = 0;
outH = 0;
return false;
}
const auto& [w, h, data] = pixmaps[bestIndex];
outW = w;
outH = h;
outArgb = data;
return true;
}
constexpr Logger kLog("tray");
constexpr Logger kLog("tray");
} // namespace
@@ -329,34 +330,34 @@ std::vector<TrayItemInfo> TrayService::items() const {
namespace {
// Recursively decode a DbusMenuLayout into the cache. Each layout node contributes
// a `std::vector<TrayMenuEntry>` keyed by its id into entriesByParent. Invisible
// entries are skipped from display but we still recurse so their own children
// (if any) are reachable from the cache.
void ingestLayoutNode(const DbusMenuLayout& node,
std::unordered_map<std::int32_t, std::vector<TrayMenuEntry>>& entriesByParent) {
const auto nodeId = std::get<0>(node);
const auto& children = std::get<2>(node);
// Recursively decode a DbusMenuLayout into the cache. Each layout node contributes
// a `std::vector<TrayMenuEntry>` keyed by its id into entriesByParent. Invisible
// entries are skipped from display but we still recurse so their own children
// (if any) are reachable from the cache.
void ingestLayoutNode(const DbusMenuLayout& node,
std::unordered_map<std::int32_t, std::vector<TrayMenuEntry>>& entriesByParent) {
const auto nodeId = std::get<0>(node);
const auto& children = std::get<2>(node);
std::vector<TrayMenuEntry> entries;
entries.reserve(children.size());
for (const auto& childValue : children) {
try {
const auto child = childValue.get<DbusMenuLayout>();
auto entry = decodeMenuEntry(child);
ingestLayoutNode(child, entriesByParent);
if (entry.id <= 0 || !entry.visible) {
continue;
std::vector<TrayMenuEntry> entries;
entries.reserve(children.size());
for (const auto& childValue : children) {
try {
const auto child = childValue.get<DbusMenuLayout>();
auto entry = decodeMenuEntry(child);
ingestLayoutNode(child, entriesByParent);
if (entry.id <= 0 || !entry.visible) {
continue;
}
if (entry.label.empty() && !entry.separator) {
continue;
}
entries.push_back(std::move(entry));
} catch (const sdbus::Error&) {
}
if (entry.label.empty() && !entry.separator) {
continue;
}
entries.push_back(std::move(entry));
} catch (const sdbus::Error&) {
}
entriesByParent[nodeId] = std::move(entries);
}
entriesByParent[nodeId] = std::move(entries);
}
} // namespace
@@ -416,8 +417,8 @@ std::vector<TrayMenuEntry> TrayService::menuEntries(const std::string& itemId) {
return {};
}
if (itemIt->second.busName.empty() || itemIt->second.menuObjectPath.empty()) {
kLog.warn("menuEntries: missing bus/menu path id={} bus='{}' menu='{}'",
itemId, itemIt->second.busName, itemIt->second.menuObjectPath);
kLog.warn("menuEntries: missing bus/menu path id={} bus='{}' menu='{}'", itemId, itemIt->second.busName,
itemIt->second.menuObjectPath);
return {};
}
@@ -469,8 +470,7 @@ std::vector<TrayMenuEntry> TrayService::menuEntriesForParent(const std::string&
return {};
}
void TrayService::ensureMenuCache(const std::string& itemId, const std::string& busName,
const std::string& menuPath) {
void TrayService::ensureMenuCache(const std::string& itemId, const std::string& busName, const std::string& menuPath) {
if (busName.empty() || menuPath.empty()) {
return;
}
@@ -480,8 +480,7 @@ void TrayService::ensureMenuCache(const std::string& itemId, const std::string&
}
try {
auto proxy = sdbus::createProxy(m_bus.connection(), sdbus::ServiceName{busName},
sdbus::ObjectPath{menuPath});
auto proxy = sdbus::createProxy(m_bus.connection(), sdbus::ServiceName{busName}, sdbus::ObjectPath{menuPath});
// LayoutUpdated(rev, parent): server is telling us the subtree rooted at
// `parent` changed. Invalidate aggressively — menus are small so re-fetch
@@ -531,17 +530,14 @@ void TrayService::sendMenuEvent(const std::string& itemId, std::int32_t entryId,
return;
}
const auto timestamp = static_cast<std::uint32_t>(
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count());
std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count());
try {
it->second.proxy->callMethod("Event")
.onInterface(k_menu_interface)
.withTimeout(std::chrono::milliseconds(500))
.withArguments(entryId, eventName, sdbus::Variant{std::int32_t{0}}, timestamp);
} catch (const sdbus::Error& e) {
kLog.debug("dbusmenu Event failed id={} entryId={} event={} err={}",
itemId, entryId, eventName, e.what());
kLog.debug("dbusmenu Event failed id={} entryId={} event={} err={}", itemId, entryId, eventName, e.what());
}
}
@@ -559,9 +555,7 @@ bool TrayService::activateMenuEntry(const std::string& itemId, std::int32_t entr
return false;
}
const auto timestamp = static_cast<std::uint32_t>(
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count());
std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count());
try {
it->second.proxy->callMethod("Event")
.onInterface(k_menu_interface)
@@ -619,8 +613,7 @@ bool TrayService::openContextMenu(const std::string& itemId, std::int32_t x, std
}
}
void TrayService::onRegisterStatusNotifierItem(const std::string& serviceOrPath,
const std::string& senderBusName) {
void TrayService::onRegisterStatusNotifierItem(const std::string& serviceOrPath, const std::string& senderBusName) {
kLog.info("RegisterStatusNotifierItem: service/path='{}' sender='{}'", serviceOrPath, senderBusName);
if (serviceOrPath.empty()) {
kLog.warn("register item ignored: empty service/path");
@@ -699,24 +692,24 @@ void TrayService::registerOrRefreshItem(const std::string& busName, const std::s
if (inserted) {
kLog.info("tray item registered id={} bus='{}' path='{}'", itemId, busName, objectPath);
m_items.emplace(itemId, TrayItemInfo{
.id = itemId,
.busName = busName,
.objectPath = objectPath,
.iconName = {},
.iconThemePath = {},
.attentionIconName = {},
.menuObjectPath = {},
.itemName = {},
.title = {},
.status = {},
.iconArgb32 = {},
.iconWidth = 0,
.iconHeight = 0,
.attentionArgb32 = {},
.attentionWidth = 0,
.attentionHeight = 0,
.needsAttention = false,
});
.id = itemId,
.busName = busName,
.objectPath = objectPath,
.iconName = {},
.iconThemePath = {},
.attentionIconName = {},
.menuObjectPath = {},
.itemName = {},
.title = {},
.status = {},
.iconArgb32 = {},
.iconWidth = 0,
.iconHeight = 0,
.attentionArgb32 = {},
.attentionWidth = 0,
.attentionHeight = 0,
.needsAttention = false,
});
if (looks_like_dbus_name(busName)) {
auto [proxyIt, _] = m_itemProxies.emplace(
@@ -739,8 +732,7 @@ void TrayService::registerOrRefreshItem(const std::string& busName, const std::s
kLog.debug("item registered: {}", itemId);
m_watcherObject->emitSignal("StatusNotifierItemRegistered").onInterface(k_watcher_interface).withArguments(itemId);
m_watcherObject->emitPropertiesChangedSignal(
k_watcher_interface,
std::vector<sdbus::PropertyName>{sdbus::PropertyName{"RegisteredStatusNotifierItems"}});
k_watcher_interface, std::vector<sdbus::PropertyName>{sdbus::PropertyName{"RegisteredStatusNotifierItems"}});
}
if (looks_like_dbus_name(busName)) {
@@ -784,9 +776,9 @@ bool TrayService::ensureItemProxy(const std::string& itemId) {
auto& item = m_items[itemId];
item.busName = candidate;
auto [proxyIt, _] = m_itemProxies.emplace(
itemId, sdbus::createProxy(m_bus.connection(), sdbus::ServiceName{candidate},
sdbus::ObjectPath{item.objectPath}));
auto [proxyIt, _] =
m_itemProxies.emplace(itemId, sdbus::createProxy(m_bus.connection(), sdbus::ServiceName{candidate},
sdbus::ObjectPath{item.objectPath}));
proxyIt->second->uponSignal("NewIcon").onInterface(k_item_interface).call([this, itemId]() {
refreshItemMetadata(itemId);
@@ -841,13 +833,13 @@ void TrayService::refreshItemMetadata(const std::string& itemId) {
auto next = cur;
// Use the existing value as the fallback so a transient D-Bus failure doesn't
// wipe out data that was successfully fetched earlier (e.g. menuObjectPath).
next.iconName = get_item_property_string_or(*proxyIt->second, "IconName", cur.iconName);
next.iconThemePath = get_item_property_string_or(*proxyIt->second, "IconThemePath", cur.iconThemePath);
next.attentionIconName= get_item_property_string_or(*proxyIt->second, "AttentionIconName",cur.attentionIconName);
next.menuObjectPath = get_item_property_string_or(*proxyIt->second, "Menu", cur.menuObjectPath);
next.itemName = get_item_property_string_or(*proxyIt->second, "Id", cur.itemName);
next.title = get_item_property_string_or(*proxyIt->second, "Title", cur.title);
next.status = get_item_property_string_or(*proxyIt->second, "Status", cur.status);
next.iconName = get_item_property_string_or(*proxyIt->second, "IconName", cur.iconName);
next.iconThemePath = get_item_property_string_or(*proxyIt->second, "IconThemePath", cur.iconThemePath);
next.attentionIconName = get_item_property_string_or(*proxyIt->second, "AttentionIconName", cur.attentionIconName);
next.menuObjectPath = get_item_property_string_or(*proxyIt->second, "Menu", cur.menuObjectPath);
next.itemName = get_item_property_string_or(*proxyIt->second, "Id", cur.itemName);
next.title = get_item_property_string_or(*proxyIt->second, "Title", cur.title);
next.status = get_item_property_string_or(*proxyIt->second, "Status", cur.status);
next.needsAttention = (next.status == "NeedsAttention");
const auto iconPixmaps = get_icon_pixmaps_or(*proxyIt->second, "IconPixmap", {});
@@ -856,12 +848,11 @@ void TrayService::refreshItemMetadata(const std::string& itemId) {
const auto attentionPixmaps = get_icon_pixmaps_or(*proxyIt->second, "AttentionIconPixmap", {});
pickBestPixmap(attentionPixmaps, next.attentionArgb32, next.attentionWidth, next.attentionHeight);
kLog.debug(
"item metadata id={} itemName='{}' status={} iconName='{}' attentionIconName='{}' menu='{}' "
"iconThemePath='{}' iconPixmap={}x{} (bytes={}) attentionPixmap={}x{} (bytes={})",
itemId, next.itemName, next.status, next.iconName, next.attentionIconName, next.menuObjectPath,
next.iconThemePath, next.iconWidth, next.iconHeight, next.iconArgb32.size(), next.attentionWidth,
next.attentionHeight, next.attentionArgb32.size());
kLog.debug("item metadata id={} itemName='{}' status={} iconName='{}' attentionIconName='{}' menu='{}' "
"iconThemePath='{}' iconPixmap={}x{} (bytes={}) attentionPixmap={}x{} (bytes={})",
itemId, next.itemName, next.status, next.iconName, next.attentionIconName, next.menuObjectPath,
next.iconThemePath, next.iconWidth, next.iconHeight, next.iconArgb32.size(), next.attentionWidth,
next.attentionHeight, next.attentionArgb32.size());
if (next == itemIt->second) {
// Menu path unchanged — make sure the cache/subscription exists (may not have
@@ -897,8 +888,9 @@ void TrayService::removeItemsForBusName(const std::string& busName) {
m_itemProxies.erase(itemId);
m_menuCache.erase(itemId);
kLog.debug("item unregistered: {}", itemId);
m_watcherObject->emitSignal("StatusNotifierItemUnregistered").onInterface(k_watcher_interface).withArguments(
itemId);
m_watcherObject->emitSignal("StatusNotifierItemUnregistered")
.onInterface(k_watcher_interface)
.withArguments(itemId);
}
m_watcherObject->emitPropertiesChangedSignal(
k_watcher_interface, std::vector<sdbus::PropertyName>{sdbus::PropertyName{"RegisteredStatusNotifierItems"}});
+1 -2
View File
@@ -3,12 +3,11 @@
#include <cstdint>
#include <functional>
#include <memory>
#include <sdbus-c++/sdbus-c++.h>
#include <string>
#include <unordered_map>
#include <vector>
#include <sdbus-c++/sdbus-c++.h>
class SessionBus;
struct TrayItemInfo {
+40 -40
View File
@@ -11,22 +11,22 @@
namespace {
static const sdbus::ServiceName k_upowerBusName{"org.freedesktop.UPower"};
static const sdbus::ObjectPath k_upowerObjectPath{"/org/freedesktop/UPower"};
static constexpr auto k_upowerInterface = "org.freedesktop.UPower";
static constexpr auto k_deviceInterface = "org.freedesktop.UPower.Device";
static constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
static const sdbus::ServiceName k_upowerBusName{"org.freedesktop.UPower"};
static const sdbus::ObjectPath k_upowerObjectPath{"/org/freedesktop/UPower"};
static constexpr auto k_upowerInterface = "org.freedesktop.UPower";
static constexpr auto k_deviceInterface = "org.freedesktop.UPower.Device";
static constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
// UPower device types
constexpr std::uint32_t k_deviceTypeBattery = 2;
// UPower device types
constexpr std::uint32_t k_deviceTypeBattery = 2;
// UPower battery states
constexpr std::uint32_t k_stateCharging = 1;
constexpr std::uint32_t k_stateDischarging = 2;
constexpr std::uint32_t k_stateEmpty = 3;
constexpr std::uint32_t k_stateFullyCharged = 4;
constexpr std::uint32_t k_statePendingCharge = 5;
constexpr std::uint32_t k_statePendingDischarge = 6;
// UPower battery states
constexpr std::uint32_t k_stateCharging = 1;
constexpr std::uint32_t k_stateDischarging = 2;
constexpr std::uint32_t k_stateEmpty = 3;
constexpr std::uint32_t k_stateFullyCharged = 4;
constexpr std::uint32_t k_statePendingCharge = 5;
constexpr std::uint32_t k_statePendingDischarge = 6;
} // namespace
@@ -52,36 +52,36 @@ std::string batteryStateLabel(BatteryState state) {
namespace {
template <typename T>
T getPropertyOr(sdbus::IProxy& proxy, std::string_view iface, std::string_view propertyName, T fallback) {
try {
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(iface);
return value.get<T>();
} catch (const sdbus::Error&) {
return fallback;
template <typename T>
T getPropertyOr(sdbus::IProxy& proxy, std::string_view iface, std::string_view propertyName, T fallback) {
try {
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(iface);
return value.get<T>();
} catch (const sdbus::Error&) {
return fallback;
}
}
}
BatteryState decodeBatteryState(std::uint32_t raw) {
switch (raw) {
case k_stateCharging:
return BatteryState::Charging;
case k_stateDischarging:
return BatteryState::Discharging;
case k_stateEmpty:
return BatteryState::Empty;
case k_stateFullyCharged:
return BatteryState::FullyCharged;
case k_statePendingCharge:
return BatteryState::PendingCharge;
case k_statePendingDischarge:
return BatteryState::PendingDischarge;
default:
return BatteryState::Unknown;
BatteryState decodeBatteryState(std::uint32_t raw) {
switch (raw) {
case k_stateCharging:
return BatteryState::Charging;
case k_stateDischarging:
return BatteryState::Discharging;
case k_stateEmpty:
return BatteryState::Empty;
case k_stateFullyCharged:
return BatteryState::FullyCharged;
case k_statePendingCharge:
return BatteryState::PendingCharge;
case k_statePendingDischarge:
return BatteryState::PendingDischarge;
default:
return BatteryState::Unknown;
}
}
}
constexpr Logger kLog("upower");
constexpr Logger kLog("upower");
} // namespace
+2 -2
View File
@@ -8,8 +8,8 @@
class SystemBus;
namespace sdbus {
class IProxy;
class ObjectPath;
class IProxy;
class ObjectPath;
} // namespace sdbus
enum class BatteryState : std::uint8_t {
+1 -1
View File
@@ -1,11 +1,11 @@
#include "idle/idle_inhibitor.h"
#include "core/log.h"
#include "idle-inhibit-unstable-v1-client-protocol.h"
#include "ipc/ipc_service.h"
#include "wayland/layer_surface.h"
#include "wayland/wayland_connection.h"
#include "idle-inhibit-unstable-v1-client-protocol.h"
#include <wayland-client.h>
namespace {
+1 -2
View File
@@ -1,9 +1,8 @@
#include "idle/idle_manager.h"
#include "core/log.h"
#include "wayland/wayland_connection.h"
#include "ext-idle-notify-v1-client-protocol.h"
#include "wayland/wayland_connection.h"
namespace {
-1
View File
@@ -3,7 +3,6 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
-1
View File
@@ -5,7 +5,6 @@
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
+1 -2
View File
@@ -1,10 +1,9 @@
#include "launcher/usage_tracker.h"
#include <json.hpp>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <json.hpp>
namespace {
+6 -3
View File
@@ -54,13 +54,16 @@ namespace {
int main(int argc, char* argv[]) {
if (argc >= 2) {
if (std::strcmp(argv[1], "theme") == 0) return noctalia::theme::runCli(argc, argv);
if (std::strcmp(argv[1], "msg") == 0) return noctalia::ipc::runCli(argc, argv);
if (std::strcmp(argv[1], "theme") == 0)
return noctalia::theme::runCli(argc, argv);
if (std::strcmp(argv[1], "msg") == 0)
return noctalia::ipc::runCli(argc, argv);
}
for (int i = 1; i < argc; ++i) {
const int rc = runTopLevelFlag(argv[i]);
if (rc >= 0) return rc;
if (rc >= 0)
return rc;
}
return runShell();
+5 -6
View File
@@ -6,6 +6,11 @@
#include "ipc/ipc_arg_parse.h"
#include "ipc/ipc_service.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <memory>
#include <optional>
#include <pipewire/extensions/metadata.h>
#include <pipewire/pipewire.h>
#include <spa/param/audio/format-utils.h>
@@ -13,12 +18,6 @@
#include <spa/pod/builder.h>
#include <spa/pod/parser.h>
#include <spa/utils/result.h>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
+4 -5
View File
@@ -3,6 +3,10 @@
#include "core/log.h"
#include "pipewire/pipewire_service.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <numbers>
#include <pipewire/core.h>
#include <pipewire/keys.h>
#include <pipewire/properties.h>
@@ -11,11 +15,6 @@
#include <spa/param/audio/raw.h>
#include <spa/param/format-utils.h>
#include <spa/pod/pod.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <numbers>
#include <utility>
namespace {
-1
View File
@@ -119,4 +119,3 @@ constexpr Color lerpColor(const Color& a, const Color& b, float t) {
.a = a.a + (b.a - a.a) * t,
};
}
+31 -35
View File
@@ -4,7 +4,6 @@
#include <cstdlib>
#include <cstring>
#include <utility>
#include <webp/decode.h>
#define WUFFS_IMPLEMENTATION
@@ -12,46 +11,43 @@
namespace {
class RgbaDecodeCallbacks final : public wuffs_aux::DecodeImageCallbacks {
public:
wuffs_base__pixel_format SelectPixfmt(
const wuffs_base__image_config& imageConfig) override {
(void)imageConfig;
return wuffs_base__make_pixel_format(WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL);
}
};
class RgbaDecodeCallbacks final : public wuffs_aux::DecodeImageCallbacks {
public:
wuffs_base__pixel_format SelectPixfmt(const wuffs_base__image_config& imageConfig) override {
(void)imageConfig;
return wuffs_base__make_pixel_format(WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL);
}
};
// Returns true if the buffer starts with the RIFF....WEBP signature.
bool isWebP(const std::uint8_t* data, std::size_t size) {
return size >= 12 &&
data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' &&
data[8] == 'W' && data[9] == 'E' && data[10] == 'B' && data[11] == 'P';
}
std::optional<DecodedRasterImage> decodeWebP(
const std::uint8_t* data, std::size_t size, std::string* errorMessage) {
int width = 0, height = 0;
std::uint8_t* rgba = WebPDecodeRGBA(data, size, &width, &height);
if (rgba == nullptr) {
if (errorMessage != nullptr)
*errorMessage = "libwebp: failed to decode WebP image";
return std::nullopt;
// Returns true if the buffer starts with the RIFF....WEBP signature.
bool isWebP(const std::uint8_t* data, std::size_t size) {
return size >= 12 && data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' && data[8] == 'W' &&
data[9] == 'E' && data[10] == 'B' && data[11] == 'P';
}
DecodedRasterImage decoded;
decoded.width = width;
decoded.height = height;
std::size_t bytes = static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4;
decoded.pixels.resize(bytes);
std::memcpy(decoded.pixels.data(), rgba, bytes);
WebPFree(rgba);
return decoded;
}
std::optional<DecodedRasterImage> decodeWebP(const std::uint8_t* data, std::size_t size, std::string* errorMessage) {
int width = 0, height = 0;
std::uint8_t* rgba = WebPDecodeRGBA(data, size, &width, &height);
if (rgba == nullptr) {
if (errorMessage != nullptr)
*errorMessage = "libwebp: failed to decode WebP image";
return std::nullopt;
}
DecodedRasterImage decoded;
decoded.width = width;
decoded.height = height;
std::size_t bytes = static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4;
decoded.pixels.resize(bytes);
std::memcpy(decoded.pixels.data(), rgba, bytes);
WebPFree(rgba);
return decoded;
}
} // namespace
std::optional<DecodedRasterImage>
decodeRasterImage(const std::uint8_t* data, std::size_t size, std::string* errorMessage) {
std::optional<DecodedRasterImage> decodeRasterImage(const std::uint8_t* data, std::size_t size,
std::string* errorMessage) {
if ((data == nullptr) || (size == 0)) {
if (errorMessage != nullptr) {
*errorMessage = "empty image buffer";
+2 -3
View File
@@ -12,6 +12,5 @@ struct DecodedRasterImage {
int height = 0;
};
[[nodiscard]] std::optional<DecodedRasterImage>
decodeRasterImage(const std::uint8_t* data, std::size_t size, std::string* errorMessage = nullptr);
[[nodiscard]] std::optional<DecodedRasterImage> decodeRasterImage(const std::uint8_t* data, std::size_t size,
std::string* errorMessage = nullptr);
+5 -10
View File
@@ -53,8 +53,7 @@ struct Mat3 {
}
[[nodiscard]] float determinant() const {
return m[0] * (m[4] * m[8] - m[7] * m[5]) - m[3] * (m[1] * m[8] - m[7] * m[2]) +
m[6] * (m[1] * m[5] - m[4] * m[2]);
return m[0] * (m[4] * m[8] - m[7] * m[5]) - m[3] * (m[1] * m[8] - m[7] * m[2]) + m[6] * (m[1] * m[5] - m[4] * m[2]);
}
[[nodiscard]] Mat3 inverse() const {
@@ -66,14 +65,10 @@ struct Mat3 {
const float invDet = 1.0f / det;
Mat3 out;
out.m = {
(m[4] * m[8] - m[7] * m[5]) * invDet,
(m[7] * m[2] - m[1] * m[8]) * invDet,
(m[1] * m[5] - m[4] * m[2]) * invDet,
(m[6] * m[5] - m[3] * m[8]) * invDet,
(m[0] * m[8] - m[6] * m[2]) * invDet,
(m[3] * m[2] - m[0] * m[5]) * invDet,
(m[3] * m[7] - m[6] * m[4]) * invDet,
(m[6] * m[1] - m[0] * m[7]) * invDet,
(m[4] * m[8] - m[7] * m[5]) * invDet, (m[7] * m[2] - m[1] * m[8]) * invDet,
(m[1] * m[5] - m[4] * m[2]) * invDet, (m[6] * m[5] - m[3] * m[8]) * invDet,
(m[0] * m[8] - m[6] * m[2]) * invDet, (m[3] * m[2] - m[0] * m[5]) * invDet,
(m[3] * m[7] - m[6] * m[4]) * invDet, (m[6] * m[1] - m[0] * m[7]) * invDet,
(m[0] * m[4] - m[3] * m[1]) * invDet,
};
return out;
+21 -21
View File
@@ -6,29 +6,29 @@
namespace {
GLuint compileShader(GLenum type, const char* source) {
const GLuint shader = glCreateShader(type);
if (shader == 0) {
throw std::runtime_error("glCreateShader failed");
GLuint compileShader(GLenum type, const char* source) {
const GLuint shader = glCreateShader(type);
if (shader == 0) {
throw std::runtime_error("glCreateShader failed");
}
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
GLint logLength = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);
std::string log(static_cast<std::size_t>(std::max(logLength, 1)), '\0');
glGetShaderInfoLog(shader, logLength, nullptr, log.data());
glDeleteShader(shader);
throw std::runtime_error("shader compilation failed: " + log);
}
return shader;
}
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
GLint logLength = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);
std::string log(static_cast<std::size_t>(std::max(logLength, 1)), '\0');
glGetShaderInfoLog(shader, logLength, nullptr, log.data());
glDeleteShader(shader);
throw std::runtime_error("shader compilation failed: " + log);
}
return shader;
}
} // namespace
ShaderProgram::~ShaderProgram() { destroy(); }
+1 -1
View File
@@ -4,7 +4,7 @@
#include "render/gl_shared_context.h"
namespace {
constexpr Logger kLog("texcache");
constexpr Logger kLog("texcache");
} // namespace
SharedTextureCache::~SharedTextureCache() {
+45 -32
View File
@@ -21,29 +21,29 @@
namespace {
constexpr Logger kLog("texture");
constexpr Logger kLog("texture");
bool endsWith(const std::string& str, const std::string& suffix) {
if (suffix.size() > str.size()) {
return false;
bool endsWith(const std::string& str, const std::string& suffix) {
if (suffix.size() > str.size()) {
return false;
}
return std::equal(suffix.rbegin(), suffix.rend(), str.rbegin());
}
return std::equal(suffix.rbegin(), suffix.rend(), str.rbegin());
}
std::vector<std::uint8_t> readFile(const std::string& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) {
return {};
std::vector<std::uint8_t> readFile(const std::string& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) {
return {};
}
const auto size = file.tellg();
if (size <= 0) {
return {};
}
std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
file.seekg(0);
file.read(reinterpret_cast<char*>(data.data()), size);
return data;
}
const auto size = file.tellg();
if (size <= 0) {
return {};
}
std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
file.seekg(0);
file.read(reinterpret_cast<char*>(data.data()), size);
return data;
}
} // namespace
@@ -118,13 +118,12 @@ TextureHandle TextureManager::loadFromFile(const std::string& path, int targetSi
return decodeEncodedRaster(fileData.data(), fileData.size(), &path, mipmap);
}
TextureHandle TextureManager::loadFromEncodedBytes(const std::uint8_t* data, std::size_t size,
bool mipmap) {
TextureHandle TextureManager::loadFromEncodedBytes(const std::uint8_t* data, std::size_t size, bool mipmap) {
return decodeEncodedRaster(data, size, nullptr, mipmap);
}
TextureHandle TextureManager::loadFromRaw(const std::uint8_t* data, std::size_t size, int width,
int height, int stride, PixmapFormat format, bool mipmap) {
TextureHandle TextureManager::loadFromRaw(const std::uint8_t* data, std::size_t size, int width, int height, int stride,
PixmapFormat format, bool mipmap) {
if (data == nullptr || size == 0 || width <= 0 || height <= 0) {
return {};
}
@@ -141,8 +140,8 @@ TextureHandle TextureManager::loadFromRaw(const std::uint8_t* data, std::size_t
const std::size_t requiredSize = (heightSize - 1U) * actualStride + minStride;
if (size < requiredSize) {
kLog.warn("raw pixmap buffer too small: width={} height={} stride={} have={} need={}", width,
height, stride, size, requiredSize);
kLog.warn("raw pixmap buffer too small: width={} height={} stride={} have={} need={}", width, height, stride, size,
requiredSize);
return {};
}
@@ -159,19 +158,34 @@ TextureHandle TextureManager::loadFromRaw(const std::uint8_t* data, std::size_t
switch (format) {
case PixmapFormat::RGBA:
d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = s[3];
d[0] = s[0];
d[1] = s[1];
d[2] = s[2];
d[3] = s[3];
break;
case PixmapFormat::BGRA:
d[0] = s[2]; d[1] = s[1]; d[2] = s[0]; d[3] = s[3];
d[0] = s[2];
d[1] = s[1];
d[2] = s[0];
d[3] = s[3];
break;
case PixmapFormat::ARGB:
d[0] = s[1]; d[1] = s[2]; d[2] = s[3]; d[3] = s[0];
d[0] = s[1];
d[1] = s[2];
d[2] = s[3];
d[3] = s[0];
break;
case PixmapFormat::RGB:
d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = 255;
d[0] = s[0];
d[1] = s[1];
d[2] = s[2];
d[3] = 255;
break;
case PixmapFormat::BGR:
d[0] = s[2]; d[1] = s[1]; d[2] = s[0]; d[3] = 255;
d[0] = s[2];
d[1] = s[1];
d[2] = s[0];
d[3] = 255;
break;
}
}
@@ -195,8 +209,7 @@ void TextureManager::cleanup() {
}
}
TextureHandle TextureManager::uploadRgba(const std::uint8_t* data, int width, int height,
bool mipmap) {
TextureHandle TextureManager::uploadRgba(const std::uint8_t* data, int width, int height, bool mipmap) {
GLuint tex = 0;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
+6 -10
View File
@@ -1,7 +1,6 @@
#pragma once
#include <GLES2/gl2.h>
#include <cstdint>
#include <string>
#include <vector>
@@ -28,19 +27,16 @@ public:
TextureManager(const TextureManager&) = delete;
TextureManager& operator=(const TextureManager&) = delete;
[[nodiscard]] TextureHandle loadFromFile(const std::string& path, int targetSize = 0,
bool mipmap = false);
[[nodiscard]] TextureHandle loadFromEncodedBytes(const std::uint8_t* data, std::size_t size,
bool mipmap = false);
[[nodiscard]] TextureHandle loadFromRaw(const std::uint8_t* data, std::size_t size, int width,
int height, int stride, PixmapFormat format,
bool mipmap = false);
[[nodiscard]] TextureHandle loadFromFile(const std::string& path, int targetSize = 0, bool mipmap = false);
[[nodiscard]] TextureHandle loadFromEncodedBytes(const std::uint8_t* data, std::size_t size, bool mipmap = false);
[[nodiscard]] TextureHandle loadFromRaw(const std::uint8_t* data, std::size_t size, int width, int height, int stride,
PixmapFormat format, bool mipmap = false);
void unload(TextureHandle& handle);
void cleanup();
private:
TextureHandle decodeEncodedRaster(const std::uint8_t* data, std::size_t size,
const std::string* debugPath = nullptr, bool mipmap = false);
TextureHandle decodeEncodedRaster(const std::uint8_t* data, std::size_t size, const std::string* debugPath = nullptr,
bool mipmap = false);
TextureHandle uploadRgba(const std::uint8_t* data, int width, int height, bool mipmap = false);
std::vector<GLuint> m_textures;
};
+9 -11
View File
@@ -4,7 +4,7 @@
namespace {
constexpr char kVertexShader[] = R"(
constexpr char kVertexShader[] = R"(
precision highp float;
attribute vec2 a_position;
varying vec2 v_texcoord;
@@ -16,9 +16,9 @@ void main() {
}
)";
// Separable Gaussian blur with a fixed 81-tap loop (-40..40).
// u_radius controls how many taps are active; taps beyond u_radius are skipped.
constexpr char kFragmentShader[] = R"(
// Separable Gaussian blur with a fixed 81-tap loop (-40..40).
// u_radius controls how many taps are active; taps beyond u_radius are skipped.
constexpr char kFragmentShader[] = R"(
precision highp float;
uniform sampler2D u_texture;
uniform vec2 u_texelSize;
@@ -52,20 +52,18 @@ void BlurProgram::ensureInitialized() {
m_program.create(kVertexShader, kFragmentShader);
const auto id = m_program.id();
m_posLoc = glGetAttribLocation(id, "a_position");
m_texLoc = glGetUniformLocation(id, "u_texture");
m_texelSzLoc = glGetUniformLocation(id, "u_texelSize");
m_posLoc = glGetAttribLocation(id, "a_position");
m_texLoc = glGetUniformLocation(id, "u_texture");
m_texelSzLoc = glGetUniformLocation(id, "u_texelSize");
m_directionLoc = glGetUniformLocation(id, "u_direction");
m_radiusLoc = glGetUniformLocation(id, "u_radius");
m_radiusLoc = glGetUniformLocation(id, "u_radius");
if (m_posLoc < 0 || m_texLoc < 0 || m_texelSzLoc < 0 || m_directionLoc < 0 || m_radiusLoc < 0) {
throw std::runtime_error("failed to query blur shader locations");
}
}
void BlurProgram::destroy() {
m_program.destroy();
}
void BlurProgram::destroy() { m_program.destroy(); }
void BlurProgram::draw(GLuint srcTex, std::uint32_t width, std::uint32_t height, float dirX, float dirY,
float radius) const {
+4 -4
View File
@@ -17,9 +17,9 @@ public:
private:
ShaderProgram m_program;
GLint m_posLoc = -1;
GLint m_texLoc = -1;
GLint m_texelSzLoc = -1;
GLint m_posLoc = -1;
GLint m_texLoc = -1;
GLint m_texelSzLoc = -1;
GLint m_directionLoc = -1;
GLint m_radiusLoc = -1;
GLint m_radiusLoc = -1;
};
+28 -30
View File
@@ -5,8 +5,8 @@
namespace {
// Positions a unit quad, applies a pixel-space transform, converts to NDC.
constexpr char kVertexShaderSource[] = R"(
// Positions a unit quad, applies a pixel-space transform, converts to NDC.
constexpr char kVertexShaderSource[] = R"(
precision highp float;
attribute vec2 a_position;
@@ -29,13 +29,13 @@ void main() {
}
)";
// Fragment shader: two modes selected by u_tint_mode.
// mode 0 (RGBA): texture stores a premultiplied RGBA glyph; scale by opacity.
// mode 1 (tint): texture stores an alpha coverage mask; the output is
// premul(tint) * coverage * opacity.
// Both paths output premultiplied to match the pipeline-wide
// GL_ONE / GL_ONE_MINUS_SRC_ALPHA blend mode.
constexpr char kFragmentShaderSource[] = R"(
// Fragment shader: two modes selected by u_tint_mode.
// mode 0 (RGBA): texture stores a premultiplied RGBA glyph; scale by opacity.
// mode 1 (tint): texture stores an alpha coverage mask; the output is
// premul(tint) * coverage * opacity.
// Both paths output premultiplied to match the pipeline-wide
// GL_ONE / GL_ONE_MINUS_SRC_ALPHA blend mode.
constexpr char kFragmentShaderSource[] = R"(
precision highp float;
uniform sampler2D u_texture;
@@ -63,15 +63,15 @@ void GlyphProgram::ensureInitialized() {
}
m_program.create(kVertexShaderSource, kFragmentShaderSource);
m_positionLocation = glGetAttribLocation(m_program.id(), "a_position");
m_texCoordLocation = glGetAttribLocation(m_program.id(), "a_texcoord");
m_positionLocation = glGetAttribLocation(m_program.id(), "a_position");
m_texCoordLocation = glGetAttribLocation(m_program.id(), "a_texcoord");
m_surfaceSizeLocation = glGetUniformLocation(m_program.id(), "u_surface_size");
m_rectLocation = glGetUniformLocation(m_program.id(), "u_size");
m_opacityLocation = glGetUniformLocation(m_program.id(), "u_opacity");
m_samplerLocation = glGetUniformLocation(m_program.id(), "u_texture");
m_rectLocation = glGetUniformLocation(m_program.id(), "u_size");
m_opacityLocation = glGetUniformLocation(m_program.id(), "u_opacity");
m_samplerLocation = glGetUniformLocation(m_program.id(), "u_texture");
m_transformLocation = glGetUniformLocation(m_program.id(), "u_transform");
m_tintLocation = glGetUniformLocation(m_program.id(), "u_tint");
m_tintModeLocation = glGetUniformLocation(m_program.id(), "u_tint_mode");
m_tintLocation = glGetUniformLocation(m_program.id(), "u_tint");
m_tintModeLocation = glGetUniformLocation(m_program.id(), "u_tint_mode");
if (m_positionLocation < 0 || m_texCoordLocation < 0 || m_surfaceSizeLocation < 0 || m_rectLocation < 0 ||
m_opacityLocation < 0 || m_samplerLocation < 0 || m_transformLocation < 0 || m_tintLocation < 0 ||
@@ -82,20 +82,19 @@ void GlyphProgram::ensureInitialized() {
void GlyphProgram::destroy() {
m_program.destroy();
m_positionLocation = -1;
m_texCoordLocation = -1;
m_positionLocation = -1;
m_texCoordLocation = -1;
m_surfaceSizeLocation = -1;
m_rectLocation = -1;
m_opacityLocation = -1;
m_samplerLocation = -1;
m_rectLocation = -1;
m_opacityLocation = -1;
m_samplerLocation = -1;
m_transformLocation = -1;
m_tintLocation = -1;
m_tintModeLocation = -1;
m_tintLocation = -1;
m_tintModeLocation = -1;
}
void GlyphProgram::bindCommon(GLuint texture, float surfaceWidth, float surfaceHeight, float width, float height,
float u0, float v0, float u1, float v1, float opacity,
const Mat3& transform) const {
float u0, float v0, float u1, float v1, float opacity, const Mat3& transform) const {
const std::array<GLfloat, 12> positions = {
0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
};
@@ -123,9 +122,8 @@ void GlyphProgram::bindCommon(GLuint texture, float surfaceWidth, float surfaceH
glDisableVertexAttribArray(texAttr);
}
void GlyphProgram::draw(GLuint texture, float surfaceWidth, float surfaceHeight, float width, float height,
float u0, float v0, float u1, float v1, float opacity,
const Mat3& transform) const {
void GlyphProgram::draw(GLuint texture, float surfaceWidth, float surfaceHeight, float width, float height, float u0,
float v0, float u1, float v1, float opacity, const Mat3& transform) const {
if (!m_program.isValid() || texture == 0 || width <= 0.0f || height <= 0.0f) {
return;
}
@@ -136,8 +134,8 @@ void GlyphProgram::draw(GLuint texture, float surfaceWidth, float surfaceHeight,
}
void GlyphProgram::drawTinted(GLuint texture, float surfaceWidth, float surfaceHeight, float width, float height,
float u0, float v0, float u1, float v1, float opacity, const Color& tint,
const Mat3& transform) const {
float u0, float v0, float u1, float v1, float opacity, const Color& tint,
const Mat3& transform) const {
if (!m_program.isValid() || texture == 0 || width <= 0.0f || height <= 0.0f) {
return;
}
+2 -2
View File
@@ -6,7 +6,7 @@
namespace {
constexpr char kVertexShaderSource[] = R"(
constexpr char kVertexShaderSource[] = R"(
precision highp float;
attribute vec2 a_position;
@@ -53,7 +53,7 @@ void main() {
}
)";
constexpr char kFragmentShaderSource[] = R"(
constexpr char kFragmentShaderSource[] = R"(
precision highp float;
uniform sampler2D u_texture;
@@ -5,7 +5,7 @@
namespace {
constexpr char kVertexShaderSource[] = R"(
constexpr char kVertexShaderSource[] = R"(
precision highp float;
attribute vec2 a_position;
@@ -25,7 +25,7 @@ void main() {
}
)";
constexpr char kFragmentShaderSource[] = R"(
constexpr char kFragmentShaderSource[] = R"(
precision highp float;
uniform vec4 u_start_color;
+3 -3
View File
@@ -5,7 +5,7 @@
namespace {
constexpr char kVertexShaderSource[] = R"(
constexpr char kVertexShaderSource[] = R"(
precision highp float;
attribute vec2 a_position;
@@ -29,7 +29,7 @@ void main() {
}
)";
constexpr char kFragmentShaderSource[] = R"(
constexpr char kFragmentShaderSource[] = R"(
precision highp float;
uniform vec2 u_rect_size;
@@ -168,7 +168,7 @@ void RectProgram::destroy() {
}
void RectProgram::draw(float surfaceWidth, float surfaceHeight, float width, float height,
const RoundedRectStyle& style, const Mat3& transform) const {
const RoundedRectStyle& style, const Mat3& transform) const {
if (!m_program.isValid() || width <= 0.0f || height <= 0.0f) {
return;
}
+2 -2
View File
@@ -48,8 +48,8 @@ struct RoundedRectStyle {
constexpr bool operator==(const RoundedRectStyle& lhs, const RoundedRectStyle& rhs) noexcept {
return lhs.fill == rhs.fill && lhs.fillEnd == rhs.fillEnd && lhs.border == rhs.border &&
lhs.fillMode == rhs.fillMode && lhs.gradientDirection == rhs.gradientDirection &&
lhs.radius == rhs.radius && lhs.softness == rhs.softness && lhs.borderWidth == rhs.borderWidth;
lhs.fillMode == rhs.fillMode && lhs.gradientDirection == rhs.gradientDirection && lhs.radius == rhs.radius &&
lhs.softness == rhs.softness && lhs.borderWidth == rhs.borderWidth;
}
class RectProgram {
+2 -2
View File
@@ -5,7 +5,7 @@
namespace {
constexpr char kVertexShaderSource[] = R"(
constexpr char kVertexShaderSource[] = R"(
precision highp float;
attribute vec2 a_position;
@@ -29,7 +29,7 @@ void main() {
}
)";
constexpr char kFragmentShaderSource[] = R"(
constexpr char kFragmentShaderSource[] = R"(
precision highp float;
uniform vec2 u_rect_size;
+10 -10
View File
@@ -4,7 +4,7 @@
namespace {
constexpr char kVertexShader[] = R"(
constexpr char kVertexShader[] = R"(
precision highp float;
attribute vec2 a_position;
varying vec2 v_texcoord;
@@ -16,9 +16,9 @@ void main() {
}
)";
// Common GLSL functions shared by all transition fragment shaders.
// Included at the top of each fragment source via string concatenation.
constexpr char kCommonFunctions[] = R"(
// Common GLSL functions shared by all transition fragment shaders.
// Included at the top of each fragment source via string concatenation.
constexpr char kCommonFunctions[] = R"(
precision highp float;
uniform sampler2D u_source1;
@@ -89,7 +89,7 @@ vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight)
}
)";
constexpr char kFadeFragment[] = R"(
constexpr char kFadeFragment[] = R"(
void main() {
vec2 uv = v_texcoord;
vec4 color1 = sampleWithFillMode(u_source1, uv, u_imageWidth1, u_imageHeight1);
@@ -98,7 +98,7 @@ void main() {
}
)";
constexpr char kWipeFragment[] = R"(
constexpr char kWipeFragment[] = R"(
uniform float u_direction;
uniform float u_smoothness;
@@ -135,7 +135,7 @@ void main() {
}
)";
constexpr char kDiscFragment[] = R"(
constexpr char kDiscFragment[] = R"(
uniform float u_smoothness;
uniform float u_centerX;
uniform float u_centerY;
@@ -166,7 +166,7 @@ void main() {
}
)";
constexpr char kStripesFragment[] = R"(
constexpr char kStripesFragment[] = R"(
uniform float u_smoothness;
uniform float u_aspectRatio;
uniform float u_stripeCount;
@@ -216,7 +216,7 @@ void main() {
}
)";
constexpr char kZoomFragment[] = R"(
constexpr char kZoomFragment[] = R"(
void main() {
vec2 uv = v_texcoord;
@@ -237,7 +237,7 @@ void main() {
}
)";
constexpr char kHoneycombFragment[] = R"(
constexpr char kHoneycombFragment[] = R"(
uniform float u_cellSize;
uniform float u_centerX;
uniform float u_centerY;
-1
View File
@@ -4,7 +4,6 @@
#include "render/core/shader_program.h"
#include <GLES2/gl2.h>
#include <array>
struct TransitionParams {
+2 -3
View File
@@ -1,4 +1,5 @@
#include "render/render_context.h"
#include "core/log.h"
#include "core/ui_phase.h"
#include "render/gl_shared_context.h"
@@ -9,17 +10,15 @@
#include "render/scene/rect_node.h"
#include "render/scene/spinner_node.h"
#include "render/scene/text_node.h"
#include "ui/style.h"
#include <GLES2/gl2.h>
#include <algorithm>
#include <cmath>
#include <optional>
#include <stdexcept>
#include <vector>
#include <GLES2/gl2.h>
namespace {
constexpr Logger kLog("render");
+1
View File
@@ -1,4 +1,5 @@
#include "render/render_target.h"
#include "render/render_context.h"
#include <wayland-egl.h>
+2 -2
View File
@@ -21,8 +21,8 @@ public:
void pointerMotion(float x, float y, std::uint32_t serial);
// Returns true if the event was consumed by a scene widget
bool pointerButton(float x, float y, std::uint32_t button, bool pressed);
bool pointerAxis(float x, float y, std::uint32_t axis, std::uint32_t axisSource, double value,
std::int32_t discrete, std::int32_t value120, float lines);
bool pointerAxis(float x, float y, std::uint32_t axis, std::uint32_t axisSource, double value, std::int32_t discrete,
std::int32_t value120, float lines);
// Dispatch keyboard events to the focused area
void keyEvent(std::uint32_t sym, std::uint32_t utf32, std::uint32_t modifiers, bool pressed, bool preedit = false);
+26 -26
View File
@@ -1,6 +1,6 @@
#include "core/ui_phase.h"
#include "render/scene/node.h"
#include "core/ui_phase.h"
#include "render/animation/animation_manager.h"
#include "render/core/mat3.h"
@@ -9,32 +9,32 @@
namespace {
Mat3 localTransform(const Node* node) {
const float cx = node->width() * 0.5f;
const float cy = node->height() * 0.5f;
return Mat3::translation(node->x(), node->y()) * Mat3::translation(cx, cy) * Mat3::rotation(node->rotation()) *
Mat3::scale(node->scale(), node->scale()) * Mat3::translation(-cx, -cy);
}
Mat3 computeWorldTransform(const Node* node) {
Mat3 world = Mat3::identity();
for (const Node* current = node; current != nullptr; current = current->parent()) {
world = localTransform(current) * world;
}
return world;
}
bool pointInsideNode(const Node* node, float sceneX, float sceneY, float& localX, float& localY) {
if (node == nullptr) {
return false;
Mat3 localTransform(const Node* node) {
const float cx = node->width() * 0.5f;
const float cy = node->height() * 0.5f;
return Mat3::translation(node->x(), node->y()) * Mat3::translation(cx, cy) * Mat3::rotation(node->rotation()) *
Mat3::scale(node->scale(), node->scale()) * Mat3::translation(-cx, -cy);
}
const Mat3 inverse = computeWorldTransform(node).inverse();
const Vec2 local = inverse.transformPoint(sceneX, sceneY);
localX = local.x;
localY = local.y;
return localX >= 0.0f && localX < node->width() && localY >= 0.0f && localY < node->height();
}
Mat3 computeWorldTransform(const Node* node) {
Mat3 world = Mat3::identity();
for (const Node* current = node; current != nullptr; current = current->parent()) {
world = localTransform(current) * world;
}
return world;
}
bool pointInsideNode(const Node* node, float sceneX, float sceneY, float& localX, float& localY) {
if (node == nullptr) {
return false;
}
const Mat3 inverse = computeWorldTransform(node).inverse();
const Vec2 local = inverse.transformPoint(sceneX, sceneY);
localX = local.x;
localY = local.y;
return localX >= 0.0f && localX < node->width() && localY >= 0.0f && localY < node->height();
}
} // namespace
@@ -307,7 +307,7 @@ bool Node::mapFromScene(const Node* node, float sceneX, float sceneY, float& out
}
void Node::transformedBounds(const Node* node, const Mat3& world, float& outLeft, float& outTop, float& outRight,
float& outBottom) {
float& outBottom) {
const Vec2 corners[] = {
world.transformPoint(0.0f, 0.0f),
world.transformPoint(node->width(), 0.0f),
+15 -20
View File
@@ -3,9 +3,8 @@
#include "core/log.h"
#include "render/programs/glyph_program.h"
#include <cairo.h>
#include <cairo-ft.h>
#include <cairo.h>
#include <ft2build.h>
#include FT_FREETYPE_H
@@ -17,22 +16,20 @@
namespace {
constexpr Logger kLog("text");
constexpr Logger kLog("text");
constexpr std::uint32_t kSizeQuant = 64;
constexpr std::uint32_t kScaleQuant = 64;
constexpr std::uint32_t kSizeQuant = 64;
constexpr std::uint32_t kScaleQuant = 64;
inline std::uint32_t quantizeSize(float v) {
return static_cast<std::uint32_t>(std::max(0.0f, v) * static_cast<float>(kSizeQuant) + 0.5f);
}
inline std::uint32_t quantizeSize(float v) {
return static_cast<std::uint32_t>(std::max(0.0f, v) * static_cast<float>(kSizeQuant) + 0.5f);
}
inline std::uint16_t quantizeScale(float v) {
return static_cast<std::uint16_t>(std::max(0.0f, v) * static_cast<float>(kScaleQuant) + 0.5f);
}
inline std::uint16_t quantizeScale(float v) {
return static_cast<std::uint16_t>(std::max(0.0f, v) * static_cast<float>(kScaleQuant) + 0.5f);
}
void hashCombine(std::size_t& seed, std::size_t v) {
seed ^= v + 0x9E3779B97F4A7C15ULL + (seed << 12) + (seed >> 4);
}
void hashCombine(std::size_t& seed, std::size_t v) { seed ^= v + 0x9E3779B97F4A7C15ULL + (seed << 12) + (seed >> 4); }
} // namespace
@@ -100,9 +97,7 @@ void CairoGlyphRenderer::setContentScale(float scale) {
}
}
void CairoGlyphRenderer::touch(CacheMap::iterator it) {
m_lru.splice(m_lru.begin(), m_lru, it->second.lruIt);
}
void CairoGlyphRenderer::touch(CacheMap::iterator it) { m_lru.splice(m_lru.begin(), m_lru, it->second.lruIt); }
void CairoGlyphRenderer::evict(CacheMap::iterator it) {
if (it->second.texture != 0) {
@@ -152,7 +147,7 @@ CairoGlyphRenderer::TextMetrics CairoGlyphRenderer::measureGlyph(char32_t codepo
out.width = static_cast<float>(m.horiAdvance) / 64.0f * invScale;
out.left = 0.0f;
out.right = w;
out.top = -bearingY; // top of ink = -bearingY relative to baseline
out.top = -bearingY; // top of ink = -bearingY relative to baseline
out.bottom = height - bearingY;
return out;
}
@@ -284,8 +279,8 @@ CairoGlyphRenderer::CacheEntry* CairoGlyphRenderer::lookupOrRasterize(char32_t c
return &ins->second;
}
void CairoGlyphRenderer::drawGlyph(float surfaceWidth, float surfaceHeight, float x, float baselineY, char32_t codepoint,
float fontSize, const Color& color, const Mat3& transform) {
void CairoGlyphRenderer::drawGlyph(float surfaceWidth, float surfaceHeight, float x, float baselineY,
char32_t codepoint, float fontSize, const Color& color, const Mat3& transform) {
if (m_face == nullptr || m_program == nullptr || codepoint == 0) {
return;
}
-1
View File
@@ -4,7 +4,6 @@
#include "render/core/mat3.h"
#include <GLES2/gl2.h>
#include <cstdint>
#include <list>
#include <string>
+73 -72
View File
@@ -3,94 +3,95 @@
#include "core/log.h"
#include "render/programs/glyph_program.h"
#include <cairo.h>
#include <fontconfig/fontconfig.h>
#include <pango/pango.h>
#include <pango/pangocairo.h>
#include <algorithm>
#include <cairo.h>
#include <cmath>
#include <cstring>
#include <fontconfig/fontconfig.h>
#include <functional>
#include <pango/pango.h>
#include <pango/pangocairo.h>
#include <vector>
namespace {
constexpr Logger kLog("text");
constexpr Logger kLog("text");
constexpr std::uint32_t kSizeQuant = 64;
constexpr std::uint32_t kScaleQuant = 64;
constexpr std::uint32_t kSizeQuant = 64;
constexpr std::uint32_t kScaleQuant = 64;
inline std::uint32_t quantizeSize(float v) {
return static_cast<std::uint32_t>(std::max(0.0f, v) * static_cast<float>(kSizeQuant) + 0.5f);
}
inline std::uint32_t quantizeSize(float v) {
return static_cast<std::uint32_t>(std::max(0.0f, v) * static_cast<float>(kSizeQuant) + 0.5f);
}
inline std::uint16_t quantizeScale(float v) {
return static_cast<std::uint16_t>(std::max(0.0f, v) * static_cast<float>(kScaleQuant) + 0.5f);
}
inline std::uint16_t quantizeScale(float v) {
return static_cast<std::uint16_t>(std::max(0.0f, v) * static_cast<float>(kScaleQuant) + 0.5f);
}
void hashCombine(std::size_t& seed, std::size_t v) {
seed ^= v + 0x9E3779B97F4A7C15ULL + (seed << 12) + (seed >> 4);
}
void hashCombine(std::size_t& seed, std::size_t v) { seed ^= v + 0x9E3779B97F4A7C15ULL + (seed << 12) + (seed >> 4); }
// Pack rgb into the top 24 bits; alpha is always forced to 0xFF so that
// opacity animations on a mixed-content string (the RGBA emoji path) reuse
// the same cache entry — the caller's alpha is applied at draw time via
// u_opacity instead of being baked into the raster.
std::uint32_t packColorRgb(const Color& c) {
const auto clamp8 = [](float v) -> std::uint32_t {
const float s = std::clamp(v, 0.0f, 1.0f);
return static_cast<std::uint32_t>(s * 255.0f + 0.5f);
};
return (clamp8(c.r) << 24) | (clamp8(c.g) << 16) | (clamp8(c.b) << 8) | 0xFFu;
}
// Pack rgb into the top 24 bits; alpha is always forced to 0xFF so that
// opacity animations on a mixed-content string (the RGBA emoji path) reuse
// the same cache entry — the caller's alpha is applied at draw time via
// u_opacity instead of being baked into the raster.
std::uint32_t packColorRgb(const Color& c) {
const auto clamp8 = [](float v) -> std::uint32_t {
const float s = std::clamp(v, 0.0f, 1.0f);
return static_cast<std::uint32_t>(s * 255.0f + 0.5f);
};
return (clamp8(c.r) << 24) | (clamp8(c.g) << 16) | (clamp8(c.b) << 8) | 0xFFu;
}
// Swap BGRA<->RGBA in place on a premultiplied ARGB32 Cairo surface buffer.
void swizzleBgraToRgba(unsigned char* data, int width, int height, int stride) {
for (int y = 0; y < height; ++y) {
unsigned char* row = data + y * stride;
for (int x = 0; x < width; ++x) {
unsigned char* p = row + x * 4;
std::swap(p[0], p[2]); // B <-> R; G and A unchanged
// Swap BGRA<->RGBA in place on a premultiplied ARGB32 Cairo surface buffer.
void swizzleBgraToRgba(unsigned char* data, int width, int height, int stride) {
for (int y = 0; y < height; ++y) {
unsigned char* row = data + y * stride;
for (int x = 0; x < width; ++x) {
unsigned char* p = row + x * 4;
std::swap(p[0], p[2]); // B <-> R; G and A unchanged
}
}
}
}
// Scan UTF-8 text for codepoints that are likely to resolve to a COLR/bitmap
// color glyph. We can't ask Pango cheaply whether a shaped run used a color
// font, so we approximate: if the text contains codepoints in the common
// emoji / symbol / dingbat ranges, rasterize as RGBA so the color layers are
// preserved. Otherwise we can use A8 coverage + shader tint, which lets one
// cache entry serve all colors for the same text.
bool containsColorGlyph(std::string_view text) {
const auto* s = reinterpret_cast<const unsigned char*>(text.data());
const std::size_t n = text.size();
std::size_t i = 0;
while (i < n) {
unsigned char b = s[i];
char32_t cp = 0;
int len = 1;
if (b < 0x80) {
cp = b;
} else if ((b & 0xE0) == 0xC0 && i + 1 < n) {
cp = static_cast<char32_t>((b & 0x1F) << 6 | (s[i + 1] & 0x3F));
len = 2;
} else if ((b & 0xF0) == 0xE0 && i + 2 < n) {
cp = static_cast<char32_t>((b & 0x0F) << 12 | (s[i + 1] & 0x3F) << 6 | (s[i + 2] & 0x3F));
len = 3;
} else if ((b & 0xF8) == 0xF0 && i + 3 < n) {
cp = static_cast<char32_t>((b & 0x07) << 18 | (s[i + 1] & 0x3F) << 12 | (s[i + 2] & 0x3F) << 6 | (s[i + 3] & 0x3F));
len = 4;
} else {
return true; // malformed — be safe
// Scan UTF-8 text for codepoints that are likely to resolve to a COLR/bitmap
// color glyph. We can't ask Pango cheaply whether a shaped run used a color
// font, so we approximate: if the text contains codepoints in the common
// emoji / symbol / dingbat ranges, rasterize as RGBA so the color layers are
// preserved. Otherwise we can use A8 coverage + shader tint, which lets one
// cache entry serve all colors for the same text.
bool containsColorGlyph(std::string_view text) {
const auto* s = reinterpret_cast<const unsigned char*>(text.data());
const std::size_t n = text.size();
std::size_t i = 0;
while (i < n) {
unsigned char b = s[i];
char32_t cp = 0;
int len = 1;
if (b < 0x80) {
cp = b;
} else if ((b & 0xE0) == 0xC0 && i + 1 < n) {
cp = static_cast<char32_t>((b & 0x1F) << 6 | (s[i + 1] & 0x3F));
len = 2;
} else if ((b & 0xF0) == 0xE0 && i + 2 < n) {
cp = static_cast<char32_t>((b & 0x0F) << 12 | (s[i + 1] & 0x3F) << 6 | (s[i + 2] & 0x3F));
len = 3;
} else if ((b & 0xF8) == 0xF0 && i + 3 < n) {
cp = static_cast<char32_t>((b & 0x07) << 18 | (s[i + 1] & 0x3F) << 12 | (s[i + 2] & 0x3F) << 6 |
(s[i + 3] & 0x3F));
len = 4;
} else {
return true; // malformed — be safe
}
i += static_cast<std::size_t>(len);
if (cp >= 0x2600 && cp <= 0x27BF)
return true; // misc symbols + dingbats
if (cp >= 0x1F000 && cp <= 0x1FFFF)
return true; // emoji planes
if (cp >= 0x1F900 && cp <= 0x1F9FF)
return true; // supplemental symbols
}
i += static_cast<std::size_t>(len);
if (cp >= 0x2600 && cp <= 0x27BF) return true; // misc symbols + dingbats
if (cp >= 0x1F000 && cp <= 0x1FFFF) return true; // emoji planes
if (cp >= 0x1F900 && cp <= 0x1F9FF) return true; // supplemental symbols
return false;
}
return false;
}
} // namespace
@@ -391,7 +392,7 @@ void CairoTextRenderer::rasterizeLayout(PangoLayout* layout, const Color& color,
struct LineSlot {
PangoLayoutLine* line = nullptr;
int xLeftPx = 0; // alignment offset of this line within the layout (pixels)
int yTopPx = 0; // top of line in full-layout raster pixels
int yTopPx = 0; // top of line in full-layout raster pixels
int baselinePx = 0; // baseline of line in full-layout raster pixels
};
struct TilePlan {
@@ -631,8 +632,8 @@ void CairoTextRenderer::draw(float surfaceWidth, float surfaceHeight, float x, f
return;
}
if (entry->tiles.size() > 1) {
kLog.warn("draw tiles={} pxW={} pxH={} baseXY=({}, {})", entry->tiles.size(), entry->pixelWidth,
entry->pixelHeight, x, baselineY);
kLog.warn("draw tiles={} pxW={} pxH={} baseXY=({}, {})", entry->tiles.size(), entry->pixelWidth, entry->pixelHeight,
x, baselineY);
}
const float invScale = 1.0f / m_contentScale;
+8 -9
View File
@@ -5,7 +5,6 @@
#include "render/core/renderer.h"
#include <GLES2/gl2.h>
#include <cstdint>
#include <list>
#include <string>
@@ -34,8 +33,8 @@ public:
float width = 0.0f;
float left = 0.0f;
float right = 0.0f;
float top = 0.0f; // negative — above baseline
float bottom = 0.0f; // positive — below baseline
float top = 0.0f; // negative — above baseline
float bottom = 0.0f; // positive — below baseline
float inkTop = 0.0f; // negative — visible ink above baseline
float inkBottom = 0.0f; // positive — visible ink below baseline
};
@@ -113,12 +112,12 @@ private:
struct CacheEntry {
std::vector<Tile> tiles;
int pixelWidth = 0; // total raster surface pixel width
int pixelHeight = 0; // total raster surface pixel height (sum of tiles)
float baselinePx = 0; // baseline from top of full layout, in raster pixels
TextMetrics metrics; // logical metrics in logical (unscaled) pixels
int pixelWidth = 0; // total raster surface pixel width
int pixelHeight = 0; // total raster surface pixel height (sum of tiles)
float baselinePx = 0; // baseline from top of full layout, in raster pixels
TextMetrics metrics; // logical metrics in logical (unscaled) pixels
std::size_t bytes = 0;
bool tinted = false; // true: GL_ALPHA coverage, tint in shader; false: premul RGBA
bool tinted = false; // true: GL_ALPHA coverage, tint in shader; false: premul RGBA
LruList::iterator lruIt;
};
@@ -147,7 +146,7 @@ private:
bool m_fontConfigInitialized = false;
std::string m_fontFamily = "sans-serif";
PangoFontMap* m_fontMap = nullptr; // owned
PangoFontMap* m_fontMap = nullptr; // owned
PangoContext* m_pangoContext = nullptr; // owned
GlyphProgram* m_program = nullptr;
+1 -2
View File
@@ -2,9 +2,8 @@
#include "render/gl_shared_context.h"
#include <stdexcept>
#include <GLES2/gl2.h>
#include <stdexcept>
#include <wayland-egl.h>
namespace {
-1
View File
@@ -1,7 +1,6 @@
#include "scripting/luau_host.h"
#include "core/log.h"
#include "lua.h"
#include "luacode.h"
#include "lualib.h"
+230 -233
View File
@@ -1,12 +1,12 @@
#include "shell/bar/bar.h"
#include "config/config_service.h"
#include "core/ui_phase.h"
#include "core/log.h"
#include "ipc/ipc_service.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 "render/scene/rect_node.h"
#include "shell/widget/widget.h"
@@ -22,227 +22,222 @@
#include "wayland/wayland_connection.h"
#include <algorithm>
#include <wayland-client-core.h>
namespace {
constexpr float kCircularCapsuleNarrowWidthEpsilon = 1.0f;
constexpr float kCircularCapsuleNarrowWidthEpsilon = 1.0f;
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");
ThemeColor withOpacity(const ThemeColor& color, float opacity) {
ThemeColor out = color;
out.alpha = std::clamp(out.alpha * std::clamp(opacity, 0.0f, 1.0f), 0.0f, 1.0f);
return out;
}
struct ShadowBleed {
std::int32_t left = 0, right = 0, up = 0, down = 0;
};
ShadowBleed computeShadowBleed(const BarConfig& cfg) {
if (cfg.shadowBlur <= 0)
return {};
return {
cfg.shadowBlur + std::max(0, -cfg.shadowOffsetX),
cfg.shadowBlur + std::max(0, cfg.shadowOffsetX),
cfg.shadowBlur + std::max(0, -cfg.shadowOffsetY),
cfg.shadowBlur + std::max(0, cfg.shadowOffsetY),
};
}
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) {
widget->layout(renderer, barAreaW, barAreaH);
std::uint32_t positionToAnchor(const std::string& position) {
if (position == "bottom") {
return LayerShellAnchor::Bottom | LayerShellAnchor::Left | LayerShellAnchor::Right;
}
};
layoutWidgets(instance.startWidgets);
layoutWidgets(instance.centerWidgets);
layoutWidgets(instance.endWidgets);
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;
}
auto finalizeCapsules = [isVertical, slotCross, &renderer](std::vector<std::unique_ptr<Widget>>& widgets) {
for (auto& w : widgets) {
if (w == nullptr || !w->barCapsuleSpec().enabled) {
continue;
constexpr Logger kLog("bar");
ThemeColor withOpacity(const ThemeColor& color, float opacity) {
ThemeColor out = color;
out.alpha = std::clamp(out.alpha * std::clamp(opacity, 0.0f, 1.0f), 0.0f, 1.0f);
return out;
}
struct ShadowBleed {
std::int32_t left = 0, right = 0, up = 0, down = 0;
};
ShadowBleed computeShadowBleed(const BarConfig& cfg) {
if (cfg.shadowBlur <= 0)
return {};
return {
cfg.shadowBlur + std::max(0, -cfg.shadowOffsetX),
cfg.shadowBlur + std::max(0, cfg.shadowOffsetX),
cfg.shadowBlur + std::max(0, -cfg.shadowOffsetY),
cfg.shadowBlur + std::max(0, cfg.shadowOffsetY),
};
}
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) {
widget->layout(renderer, barAreaW, barAreaH);
}
Node* shell = w->barCapsuleShell();
Box* bg = w->barCapsuleBox();
Node* inner = w->root();
if (shell == nullptr || bg == nullptr || inner == nullptr) {
continue;
}
// Keep the capsule shell visibility in sync with the inner root so that
// hidden widgets don't occupy phantom space in the flex layout.
shell->setVisible(inner->visible());
const float scale = w->contentScale();
const float iw = inner->width();
const float ih = inner->height();
if (!w->shouldShowBarCapsule()) {
shell->setSize(iw, ih);
inner->setPosition(0.0f, 0.0f);
bg->setVisible(false);
bg->setPosition(0.0f, 0.0f);
bg->setSize(iw, ih);
continue;
}
// Uniform capsule body extent on the cross axis — matches the reference "A"
// height used by Glyph/Label so all capsules share the same cross size
// regardless of each widget's content height.
const auto refMetrics = renderer.measureText("A", Style::fontSizeBody * scale);
const float bodyExtent = std::round(refMetrics.bottom - refMetrics.top);
const float pad = w->barCapsuleSpec().padding * scale;
const float padMain = pad;
const float padCross = std::min(pad, Style::spaceXs * scale);
float shellMain = (isVertical ? ih : iw) + 2.0f * padMain;
float shellCross = bodyExtent + 2.0f * padCross;
if (isVertical) {
shellCross = std::min(shellCross, slotCross);
}
float shellW = isVertical ? shellCross : shellMain;
float shellH = isVertical ? shellMain : shellCross;
float innerX = std::round((shellW - iw) * 0.5f);
float innerY = std::round((shellH - ih) * 0.5f);
// Glyph-only widgets have content close to bodyExtent on both axes — round
// them into a circular capsule. Multi-line / wide content (e.g. stacked
// vertical clock) must NOT be squared, or the capsule collapses on the
// main axis.
const float iconThreshold = bodyExtent + (kCircularCapsuleNarrowWidthEpsilon * scale);
const bool iconSized = iw <= iconThreshold && ih <= iconThreshold;
if (iconSized) {
float side = std::max(shellW, shellH);
if (isVertical) {
side = std::min(side, slotCross);
};
layoutWidgets(instance.startWidgets);
layoutWidgets(instance.centerWidgets);
layoutWidgets(instance.endWidgets);
auto finalizeCapsules = [isVertical, slotCross, &renderer](std::vector<std::unique_ptr<Widget>>& widgets) {
for (auto& w : widgets) {
if (w == nullptr || !w->barCapsuleSpec().enabled) {
continue;
}
shellW = side;
shellH = side;
innerX = std::round((shellW - iw) * 0.5f);
innerY = std::round((shellH - ih) * 0.5f);
Node* shell = w->barCapsuleShell();
Box* bg = w->barCapsuleBox();
Node* inner = w->root();
if (shell == nullptr || bg == nullptr || inner == nullptr) {
continue;
}
// Keep the capsule shell visibility in sync with the inner root so that
// hidden widgets don't occupy phantom space in the flex layout.
shell->setVisible(inner->visible());
const float scale = w->contentScale();
const float iw = inner->width();
const float ih = inner->height();
if (!w->shouldShowBarCapsule()) {
shell->setSize(iw, ih);
inner->setPosition(0.0f, 0.0f);
bg->setVisible(false);
bg->setPosition(0.0f, 0.0f);
bg->setSize(iw, ih);
continue;
}
// Uniform capsule body extent on the cross axis — matches the reference "A"
// height used by Glyph/Label so all capsules share the same cross size
// regardless of each widget's content height.
const auto refMetrics = renderer.measureText("A", Style::fontSizeBody * scale);
const float bodyExtent = std::round(refMetrics.bottom - refMetrics.top);
const float pad = w->barCapsuleSpec().padding * scale;
const float padMain = pad;
const float padCross = std::min(pad, Style::spaceXs * scale);
float shellMain = (isVertical ? ih : iw) + 2.0f * padMain;
float shellCross = bodyExtent + 2.0f * padCross;
if (isVertical) {
shellCross = std::min(shellCross, slotCross);
}
float shellW = isVertical ? shellCross : shellMain;
float shellH = isVertical ? shellMain : shellCross;
float innerX = std::round((shellW - iw) * 0.5f);
float innerY = std::round((shellH - ih) * 0.5f);
// Glyph-only widgets have content close to bodyExtent on both axes — round
// them into a circular capsule. Multi-line / wide content (e.g. stacked
// vertical clock) must NOT be squared, or the capsule collapses on the
// main axis.
const float iconThreshold = bodyExtent + (kCircularCapsuleNarrowWidthEpsilon * scale);
const bool iconSized = iw <= iconThreshold && ih <= iconThreshold;
if (iconSized) {
float side = std::max(shellW, shellH);
if (isVertical) {
side = std::min(side, slotCross);
}
shellW = side;
shellH = side;
innerX = std::round((shellW - iw) * 0.5f);
innerY = std::round((shellH - ih) * 0.5f);
}
shell->setSize(shellW, shellH);
bg->setVisible(true);
bg->setPosition(0.0f, 0.0f);
bg->setSize(shellW, shellH);
inner->setPosition(innerX, innerY);
bg->setRadius(std::min(shellW, shellH) * 0.5f);
}
};
finalizeCapsules(instance.startWidgets);
finalizeCapsules(instance.centerWidgets);
finalizeCapsules(instance.endWidgets);
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;
}
shell->setSize(shellW, shellH);
bg->setVisible(true);
bg->setPosition(0.0f, 0.0f);
bg->setSize(shellW, shellH);
inner->setPosition(innerX, innerY);
bg->setRadius(std::min(shellW, shellH) * 0.5f);
}
};
finalizeCapsules(instance.startWidgets);
finalizeCapsules(instance.centerWidgets);
finalizeCapsules(instance.endWidgets);
const float contentMainStart = padding;
const float contentMainEnd = std::max(contentMainStart, (isVertical ? barAreaH : barAreaW) - padding);
const float contentMainSpan = std::max(0.0f, contentMainEnd - contentMainStart);
const float barMidline = contentMainStart + contentMainSpan * 0.5f;
const float centerNaturalMain = isVertical ? instance.centerSection->height() : instance.centerSection->width();
auto configureSlot = [&](Node* slot, float mainOffset, float mainSize) {
slot->setClipChildren(true);
if (isVertical) {
slot->setPosition(0.0f, mainOffset);
slot->setSize(slotCross, mainSize);
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 {
slot->setPosition(mainOffset, 0.0f);
slot->setSize(mainSize, slotCross);
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);
auto configureSection = [&](Flex* section, FlexJustify justify) {
section->setJustify(justify);
section->layout(renderer);
};
configureSlot(instance.startSlot, contentMainStart, startSlotMain);
configureSlot(instance.centerSlot, centerSlotStart, centerSlotMain);
configureSlot(instance.endSlot, centerSlotEnd, endSlotMain);
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;
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);
}
}
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);
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(); });
}
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
@@ -252,10 +247,10 @@ bool Bar::initialize(WaylandConnection& wayland, ConfigService* config, TimeServ
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) {
PipeWireSpectrum* audioSpectrum, HttpClient* httpClient, WeatherService* weatherService,
RenderContext* renderContext, NightLightManager* nightLight,
noctalia::theme::ThemeService* themeService, BluetoothService* bluetooth,
BrightnessService* brightness) {
m_wayland = &wayland;
m_config = config;
m_time = timeService;
@@ -458,42 +453,42 @@ void Bar::createInstance(const WaylandOutput& output, const BarConfig& barConfig
std::int32_t exclusiveZone = 0;
if (!vertical) {
mLeft = std::max(0, mH - sb.left);
mLeft = std::max(0, mH - sb.left);
mRight = std::max(0, mH - sb.right);
if (isBottom) {
mBottom = std::max(0, mV - sb.down);
surfH = static_cast<std::uint32_t>(sb.up + barConfig.thickness + std::min(mV, sb.down));
mBottom = std::max(0, mV - sb.down);
surfH = static_cast<std::uint32_t>(sb.up + barConfig.thickness + std::min(mV, sb.down));
exclusiveZone = barConfig.thickness + std::min(mV, sb.down);
} else {
mTop = std::max(0, mV - sb.up);
surfH = static_cast<std::uint32_t>(std::min(mV, sb.up) + barConfig.thickness + sb.down);
mTop = std::max(0, mV - sb.up);
surfH = static_cast<std::uint32_t>(std::min(mV, sb.up) + barConfig.thickness + sb.down);
exclusiveZone = std::min(mV, sb.up) + barConfig.thickness;
}
} else {
mTop = std::max(0, mV - sb.up);
mTop = std::max(0, mV - sb.up);
mBottom = std::max(0, mV - sb.down);
if (isRight) {
mRight = std::max(0, mH - sb.right);
surfW = static_cast<std::uint32_t>(sb.left + barConfig.thickness + std::min(mH, sb.right));
mRight = std::max(0, mH - sb.right);
surfW = static_cast<std::uint32_t>(sb.left + barConfig.thickness + std::min(mH, sb.right));
exclusiveZone = barConfig.thickness + std::min(mH, sb.right);
} else {
mLeft = std::max(0, mH - sb.left);
surfW = static_cast<std::uint32_t>(std::min(mH, sb.left) + barConfig.thickness + sb.right);
mLeft = std::max(0, mH - sb.left);
surfW = static_cast<std::uint32_t>(std::min(mH, sb.left) + barConfig.thickness + sb.right);
exclusiveZone = std::min(mH, sb.left) + barConfig.thickness;
}
}
auto surfaceConfig = LayerSurfaceConfig{
.nameSpace = "noctalia-" + barConfig.name,
.layer = LayerShellLayer::Top,
.anchor = anchor,
.width = surfW,
.height = surfH,
.nameSpace = "noctalia-" + barConfig.name,
.layer = LayerShellLayer::Top,
.anchor = anchor,
.width = surfW,
.height = surfH,
.exclusiveZone = exclusiveZone,
.marginTop = mTop,
.marginRight = mRight,
.marginBottom = mBottom,
.marginLeft = mLeft,
.marginTop = mTop,
.marginRight = mRight,
.marginBottom = mBottom,
.marginLeft = mLeft,
.defaultHeight = surfH,
};
@@ -556,7 +551,9 @@ void Bar::populateWidgets(BarInstance& instance) {
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::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) ||
@@ -602,10 +599,10 @@ void Bar::buildScene(BarInstance& instance, std::uint32_t width, std::uint32_t h
// Shadow bleed in each direction (matches createInstance geometry).
const auto sbi = computeShadowBleed(instance.barConfig);
const float bleedLeft = static_cast<float>(sbi.left);
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);
const float bleedUp = static_cast<float>(sbi.up);
const float bleedDown = static_cast<float>(sbi.down);
// The bar's visual area within the tight surface.
// compositor margins absorbed the outer gap; only the shadow bleed (capped at the gap)
@@ -714,9 +711,9 @@ void Bar::buildScene(BarInstance& instance, std::uint32_t width, std::uint32_t h
// Fade-in animation
instance.sceneRoot->setOpacity(0.0f);
instance.animations.animate(0.0f, 1.0f, Style::animSlow, Easing::EaseOutCubic,
[root = instance.sceneRoot.get()](float v) { root->setOpacity(v); }, {},
instance.sceneRoot.get());
instance.animations.animate(
0.0f, 1.0f, Style::animSlow, Easing::EaseOutCubic,
[root = instance.sceneRoot.get()](float v) { root->setOpacity(v); }, {}, instance.sceneRoot.get());
instance.surface->setSceneRoot(instance.sceneRoot.get());
}
@@ -804,10 +801,10 @@ void Bar::updateWidgets(BarInstance& instance) {
const float marginV = static_cast<float>(instance.barConfig.marginV);
const bool isVertical = (instance.barConfig.position == "left" || instance.barConfig.position == "right");
const auto sbi = computeShadowBleed(instance.barConfig);
const float bleedLeft = static_cast<float>(sbi.left);
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);
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(marginV, bleedUp);
+6 -7
View File
@@ -38,13 +38,12 @@ public:
Bar();
bool 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);
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);
void reload();
void closeAllInstances();
void show();
+1 -2
View File
@@ -5,9 +5,8 @@
#include "render/scene/input_dispatcher.h"
#include "render/scene/node.h"
#include "shell/widget/widget.h"
#include "wayland/layer_surface.h"
#include "ui/signal.h"
#include "wayland/layer_surface.h"
#include <cstdint>
#include <memory>
+58 -62
View File
@@ -7,6 +7,7 @@
#include "render/scene/input_area.h"
#include "shell/control_center/tab.h"
#include "shell/panel/panel_manager.h"
#include "time/time_service.h"
#include "ui/controls/button.h"
#include "ui/controls/flex.h"
#include "ui/controls/glyph.h"
@@ -16,7 +17,6 @@
#include "ui/controls/scroll_view.h"
#include "ui/palette.h"
#include "ui/style.h"
#include "time/time_service.h"
#include "wayland/clipboard_service.h"
#include <algorithm>
@@ -30,72 +30,71 @@
namespace {
constexpr float kSidebarWidth = 272.0f;
constexpr float kRowHeight = 46.0f;
constexpr float kPreviewImageHeight = 280.0f;
constexpr float kListGlyphSize = 24.0f;
constexpr auto kPreviewPayloadDebounceInterval = std::chrono::milliseconds(75);
constexpr auto kFilterDebounceInterval = std::chrono::milliseconds(120);
constexpr float kSidebarWidth = 272.0f;
constexpr float kRowHeight = 46.0f;
constexpr float kPreviewImageHeight = 280.0f;
constexpr float kListGlyphSize = 24.0f;
constexpr auto kPreviewPayloadDebounceInterval = std::chrono::milliseconds(75);
constexpr auto kFilterDebounceInterval = std::chrono::milliseconds(120);
std::string collapseWhitespace(std::string_view text) {
std::string out;
out.reserve(text.size());
std::string collapseWhitespace(std::string_view text) {
std::string out;
out.reserve(text.size());
bool lastWasSpace = true;
for (char ch : text) {
const bool isWhitespace = (ch == ' ' || ch == '\n' || ch == '\t' || ch == '\r');
if (isWhitespace) {
if (!lastWasSpace) {
out.push_back(' ');
bool lastWasSpace = true;
for (char ch : text) {
const bool isWhitespace = (ch == ' ' || ch == '\n' || ch == '\t' || ch == '\r');
if (isWhitespace) {
if (!lastWasSpace) {
out.push_back(' ');
}
lastWasSpace = true;
continue;
}
lastWasSpace = true;
continue;
out.push_back(ch);
lastWasSpace = false;
}
out.push_back(ch);
lastWasSpace = false;
if (!out.empty() && out.back() == ' ') {
out.pop_back();
}
return out;
}
if (!out.empty() && out.back() == ' ') {
out.pop_back();
}
return out;
}
std::string formatBytes(std::size_t bytes) {
const char* units[] = {"B", "KB", "MB", "GB"};
double value = static_cast<double>(bytes);
std::size_t unitIndex = 0;
while (value >= 1024.0 && unitIndex + 1 < std::size(units)) {
value /= 1024.0;
++unitIndex;
}
std::string formatBytes(std::size_t bytes) {
const char* units[] = {"B", "KB", "MB", "GB"};
double value = static_cast<double>(bytes);
std::size_t unitIndex = 0;
while (value >= 1024.0 && unitIndex + 1 < std::size(units)) {
value /= 1024.0;
++unitIndex;
char buffer[32];
if (unitIndex == 0) {
std::snprintf(buffer, sizeof(buffer), "%zu %s", bytes, units[unitIndex]);
} else {
std::snprintf(buffer, sizeof(buffer), "%.1f %s", value, units[unitIndex]);
}
return buffer;
}
char buffer[32];
if (unitIndex == 0) {
std::snprintf(buffer, sizeof(buffer), "%zu %s", bytes, units[unitIndex]);
} else {
std::snprintf(buffer, sizeof(buffer), "%.1f %s", value, units[unitIndex]);
std::string entryTitle(const ClipboardEntry& entry) {
if (!entry.textPreview.empty()) {
return entry.textPreview;
}
if (entry.isImage()) {
return i18n::tr("clipboard.entry-image");
}
return entry.dataMimeType.empty() ? i18n::tr("clipboard.entry-title") : entry.dataMimeType;
}
return buffer;
}
std::string entryTitle(const ClipboardEntry& entry) {
if (!entry.textPreview.empty()) {
return entry.textPreview;
std::string previewTitle(const ClipboardEntry& entry) {
if (entry.isImage()) {
return i18n::tr("clipboard.preview-image-title");
}
return i18n::tr("clipboard.preview-text-title");
}
if (entry.isImage()) {
return i18n::tr("clipboard.entry-image");
}
return entry.dataMimeType.empty() ? i18n::tr("clipboard.entry-title") : entry.dataMimeType;
}
std::string previewTitle(const ClipboardEntry& entry) {
if (entry.isImage()) {
return i18n::tr("clipboard.preview-image-title");
}
return i18n::tr("clipboard.preview-text-title");
}
} // namespace
@@ -170,9 +169,8 @@ void ClipboardPanel::create() {
filterInput->setHorizontalPadding(Style::spaceMd * scale);
filterInput->setOnChange([this](const std::string& text) { onFilterChanged(text); });
filterInput->setOnSubmit([this](const std::string& /*text*/) { activateSelected(); });
filterInput->setOnKeyEvent([this](std::uint32_t sym, std::uint32_t modifiers) {
return handleKeyEvent(sym, modifiers);
});
filterInput->setOnKeyEvent(
[this](std::uint32_t sym, std::uint32_t modifiers) { return handleKeyEvent(sym, modifiers); });
m_filterInput = filterInput.get();
sidebar->addChild(std::move(filterInput));
@@ -287,8 +285,7 @@ void ClipboardPanel::doLayout(Renderer& renderer, float width, float height) {
}
const float previewScrollH = m_previewScrollView->height();
if (m_lastPreviewWidth != m_previewScrollView->contentViewportWidth() ||
m_lastPreviewHeight != previewScrollH) {
if (m_lastPreviewWidth != m_previewScrollView->contentViewportWidth() || m_lastPreviewHeight != previewScrollH) {
rebuildPreview(renderer, m_previewScrollView->contentViewportWidth(), previewScrollH);
relayoutNeeded = true;
}
@@ -451,8 +448,7 @@ void ClipboardPanel::rebuildList(Renderer& renderer, float width) {
row->setDirection(FlexDirection::Horizontal);
row->setAlign(FlexAlign::Center);
row->setGap(Style::spaceMd);
row->setPadding(Style::spaceXs, Style::spaceSm,
Style::spaceXs, Style::spaceSm);
row->setPadding(Style::spaceXs, Style::spaceSm, Style::spaceXs, Style::spaceSm);
row->setSize(width, 0.0f);
row->setFillParentMainAxis(true);
row->setMinHeight(kRowHeight);
+1 -1
View File
@@ -1,7 +1,7 @@
#pragma once
#include "shell/panel/panel.h"
#include "core/timer_manager.h"
#include "shell/panel/panel.h"
#include "wayland/clipboard_service.h"
#include <cstddef>
+51 -51
View File
@@ -8,64 +8,64 @@
namespace clipboard_paste {
namespace {
namespace {
constexpr Logger kLog("clipboard");
constexpr Logger kLog("clipboard");
std::optional<VirtualPasteShortcut> virtualPasteShortcutFor(ClipboardAutoPasteMode mode, bool isImage) {
switch (mode) {
case ClipboardAutoPasteMode::Off:
std::optional<VirtualPasteShortcut> virtualPasteShortcutFor(ClipboardAutoPasteMode mode, bool isImage) {
switch (mode) {
case ClipboardAutoPasteMode::Off:
return std::nullopt;
case ClipboardAutoPasteMode::Auto:
return isImage ? VirtualPasteShortcut::CtrlV : VirtualPasteShortcut::CtrlShiftV;
case ClipboardAutoPasteMode::CtrlV:
return VirtualPasteShortcut::CtrlV;
case ClipboardAutoPasteMode::CtrlShiftV:
return VirtualPasteShortcut::CtrlShiftV;
case ClipboardAutoPasteMode::ShiftInsert:
return VirtualPasteShortcut::ShiftInsert;
}
return std::nullopt;
case ClipboardAutoPasteMode::Auto:
return isImage ? VirtualPasteShortcut::CtrlV : VirtualPasteShortcut::CtrlShiftV;
case ClipboardAutoPasteMode::CtrlV:
return VirtualPasteShortcut::CtrlV;
case ClipboardAutoPasteMode::CtrlShiftV:
return VirtualPasteShortcut::CtrlShiftV;
case ClipboardAutoPasteMode::ShiftInsert:
return VirtualPasteShortcut::ShiftInsert;
}
return std::nullopt;
}
std::vector<std::string> wtypeArgsFor(ClipboardAutoPasteMode mode, bool isImage) {
switch (mode) {
case ClipboardAutoPasteMode::Off:
std::vector<std::string> wtypeArgsFor(ClipboardAutoPasteMode mode, bool isImage) {
switch (mode) {
case ClipboardAutoPasteMode::Off:
return {};
case ClipboardAutoPasteMode::Auto:
return isImage ? std::vector<std::string>{"wtype", "-M", "ctrl", "-k", "v", "-m", "ctrl"}
: std::vector<std::string>{"wtype", "-M", "ctrl", "-M", "shift", "-k",
"v", "-m", "shift", "-m", "ctrl"};
case ClipboardAutoPasteMode::CtrlV:
return {"wtype", "-M", "ctrl", "-k", "v", "-m", "ctrl"};
case ClipboardAutoPasteMode::CtrlShiftV:
return {"wtype", "-M", "ctrl", "-M", "shift", "-k", "v", "-m", "shift", "-m", "ctrl"};
case ClipboardAutoPasteMode::ShiftInsert:
return {"wtype", "-M", "shift", "-k", "Insert", "-m", "shift"};
}
return {};
case ClipboardAutoPasteMode::Auto:
return isImage ? std::vector<std::string>{"wtype", "-M", "ctrl", "-k", "v", "-m", "ctrl"}
: std::vector<std::string>{"wtype", "-M", "ctrl", "-M", "shift", "-k",
"v", "-m", "shift", "-m", "ctrl"};
case ClipboardAutoPasteMode::CtrlV:
return {"wtype", "-M", "ctrl", "-k", "v", "-m", "ctrl"};
case ClipboardAutoPasteMode::CtrlShiftV:
return {"wtype", "-M", "ctrl", "-M", "shift", "-k", "v", "-m", "shift", "-m", "ctrl"};
case ClipboardAutoPasteMode::ShiftInsert:
return {"wtype", "-M", "shift", "-k", "Insert", "-m", "shift"};
}
return {};
} // namespace
bool pasteEntry(const ClipboardEntry& entry, ClipboardAutoPasteMode mode, VirtualKeyboardService& virtualKeyboard) {
if (mode == ClipboardAutoPasteMode::Off) {
return true;
}
const bool isImage = entry.isImage();
const auto shortcut = virtualPasteShortcutFor(mode, isImage);
if (shortcut.has_value() && virtualKeyboard.sendPasteShortcut(*shortcut)) {
return true;
}
const auto args = wtypeArgsFor(mode, isImage);
if (!args.empty() && process::launchDetached(args)) {
return true;
}
kLog.warn("clipboard auto-paste failed: native virtual keyboard unavailable and wtype launch failed");
return false;
}
} // namespace
bool pasteEntry(const ClipboardEntry& entry, ClipboardAutoPasteMode mode, VirtualKeyboardService& virtualKeyboard) {
if (mode == ClipboardAutoPasteMode::Off) {
return true;
}
const bool isImage = entry.isImage();
const auto shortcut = virtualPasteShortcutFor(mode, isImage);
if (shortcut.has_value() && virtualKeyboard.sendPasteShortcut(*shortcut)) {
return true;
}
const auto args = wtypeArgsFor(mode, isImage);
if (!args.empty() && process::launchDetached(args)) {
return true;
}
kLog.warn("clipboard auto-paste failed: native virtual keyboard unavailable and wtype launch failed");
return false;
}
} // namespace clipboard_paste
+181 -186
View File
@@ -1,7 +1,7 @@
#include "shell/control_center/audio_tab.h"
#include "core/ui_phase.h"
#include "config/config_service.h"
#include "core/ui_phase.h"
#include "pipewire/pipewire_service.h"
#include "render/core/renderer.h"
#include "render/scene/input_area.h"
@@ -25,194 +25,194 @@ using namespace control_center;
namespace {
constexpr float kDevicesColumnGrow = 3.0f;
constexpr float kValueLabelWidth = Style::controlHeightLg + Style::spaceLg;
constexpr float kVolumeSyncEpsilon = 0.005f; // 0.5%
constexpr auto kVolumeCommitInterval = std::chrono::milliseconds(16);
constexpr auto kVolumeStateHoldoff = std::chrono::milliseconds(180);
constexpr float kDevicesColumnGrow = 3.0f;
constexpr float kValueLabelWidth = Style::controlHeightLg + Style::spaceLg;
constexpr float kVolumeSyncEpsilon = 0.005f; // 0.5%
constexpr auto kVolumeCommitInterval = std::chrono::milliseconds(16);
constexpr auto kVolumeStateHoldoff = std::chrono::milliseconds(180);
class AudioDeviceRow : public Flex {
public:
explicit AudioDeviceRow(std::function<void()> onSelect) : m_onSelect(std::move(onSelect)) {
setDirection(FlexDirection::Horizontal);
setAlign(FlexAlign::Center);
setGap(Style::spaceSm);
setPadding(Style::spaceSm, Style::spaceMd);
setMinHeight(Style::controlHeightLg);
setRadius(Style::radiusMd);
setBackground(roleColor(ColorRole::Surface));
setBorderWidth(0.0f);
class AudioDeviceRow : public Flex {
public:
explicit AudioDeviceRow(std::function<void()> onSelect) : m_onSelect(std::move(onSelect)) {
setDirection(FlexDirection::Horizontal);
setAlign(FlexAlign::Center);
setGap(Style::spaceSm);
setPadding(Style::spaceSm, Style::spaceMd);
setMinHeight(Style::controlHeightLg);
setRadius(Style::radiusMd);
setBackground(roleColor(ColorRole::Surface));
setBorderWidth(0.0f);
auto radio = std::make_unique<RadioButton>();
radio->setOnChange([this](bool /*checked*/) {
if (m_onSelect) {
m_onSelect();
}
});
m_radio = static_cast<RadioButton*>(addChild(std::move(radio)));
auto radio = std::make_unique<RadioButton>();
radio->setOnChange([this](bool /*checked*/) {
if (m_onSelect) {
m_onSelect();
}
});
m_radio = static_cast<RadioButton*>(addChild(std::move(radio)));
auto title = std::make_unique<Label>();
title->setBold(true);
title->setFontSize(Style::fontSizeBody);
title->setColor(roleColor(ColorRole::OnSurface));
title->setFlexGrow(1.0f);
m_title = title.get();
addChild(std::move(title));
auto title = std::make_unique<Label>();
title->setBold(true);
title->setFontSize(Style::fontSizeBody);
title->setColor(roleColor(ColorRole::OnSurface));
title->setFlexGrow(1.0f);
m_title = title.get();
addChild(std::move(title));
m_detail = nullptr; // Remove detail label (subtext)
m_detail = nullptr; // Remove detail label (subtext)
auto area = std::make_unique<InputArea>();
area->setPropagateEvents(true);
area->setOnEnter([this](const InputArea::PointerData& /*data*/) { applyState(); });
area->setOnLeave([this]() { applyState(); });
area->setOnPress([this](const InputArea::PointerData& /*data*/) { applyState(); });
area->setOnClick([this](const InputArea::PointerData& /*data*/) {
if (m_onSelect) {
m_onSelect();
}
});
m_inputArea = static_cast<InputArea*>(addChild(std::move(area)));
auto area = std::make_unique<InputArea>();
area->setPropagateEvents(true);
area->setOnEnter([this](const InputArea::PointerData& /*data*/) { applyState(); });
area->setOnLeave([this]() { applyState(); });
area->setOnPress([this](const InputArea::PointerData& /*data*/) { applyState(); });
area->setOnClick([this](const InputArea::PointerData& /*data*/) {
if (m_onSelect) {
m_onSelect();
}
});
m_inputArea = static_cast<InputArea*>(addChild(std::move(area)));
applyState();
m_paletteConn = paletteChanged().connect([this] { applyState(); });
}
void setDevice(const AudioNode& node) {
m_radio->setChecked(node.isDefault);
const std::string title = !node.description.empty() ? node.description : node.name;
if (m_title != nullptr) {
m_title->setText(title);
}
}
void doLayout(Renderer& renderer) override {
if (m_radio == nullptr || m_title == nullptr || m_inputArea == nullptr) {
return;
applyState();
m_paletteConn = paletteChanged().connect([this] { applyState(); });
}
m_radio->layout(renderer);
void setDevice(const AudioNode& node) {
m_radio->setChecked(node.isDefault);
const std::string title = !node.description.empty() ? node.description : node.name;
const float textMaxWidth =
std::max(0.0f, width() - paddingLeft() - paddingRight() - gap() - m_radio->width());
m_title->setMaxWidth(textMaxWidth);
m_inputArea->setVisible(false);
Flex::doLayout(renderer);
m_inputArea->setVisible(true);
m_inputArea->setPosition(0.0f, 0.0f);
m_inputArea->setSize(width(), height());
applyState();
}
private:
void applyState() {
if (pressed()) {
setBackground(roleColor(ColorRole::Primary));
setBorderColor(roleColor(ColorRole::Primary));
setBorderWidth(Style::borderWidth);
if (m_title != nullptr) {
m_title->setColor(roleColor(ColorRole::OnPrimary));
m_title->setText(title);
}
return;
}
setBackground(roleColor(ColorRole::Surface));
setBorderColor(roleColor(hovered() ? ColorRole::Primary : ColorRole::Surface));
setBorderWidth(hovered() ? Style::borderWidth : 0.0f);
if (m_title != nullptr) {
m_title->setColor(roleColor(ColorRole::OnSurface));
void doLayout(Renderer& renderer) override {
if (m_radio == nullptr || m_title == nullptr || m_inputArea == nullptr) {
return;
}
m_radio->layout(renderer);
const float textMaxWidth = std::max(0.0f, width() - paddingLeft() - paddingRight() - gap() - m_radio->width());
m_title->setMaxWidth(textMaxWidth);
m_inputArea->setVisible(false);
Flex::doLayout(renderer);
m_inputArea->setVisible(true);
m_inputArea->setPosition(0.0f, 0.0f);
m_inputArea->setSize(width(), height());
applyState();
}
private:
void applyState() {
if (pressed()) {
setBackground(roleColor(ColorRole::Primary));
setBorderColor(roleColor(ColorRole::Primary));
setBorderWidth(Style::borderWidth);
if (m_title != nullptr) {
m_title->setColor(roleColor(ColorRole::OnPrimary));
}
return;
}
setBackground(roleColor(ColorRole::Surface));
setBorderColor(roleColor(hovered() ? ColorRole::Primary : ColorRole::Surface));
setBorderWidth(hovered() ? Style::borderWidth : 0.0f);
if (m_title != nullptr) {
m_title->setColor(roleColor(ColorRole::OnSurface));
}
}
[[nodiscard]] bool hovered() const noexcept { return m_inputArea != nullptr && m_inputArea->hovered(); }
[[nodiscard]] bool pressed() const noexcept { return m_inputArea != nullptr && m_inputArea->pressed(); }
std::function<void()> m_onSelect;
RadioButton* m_radio = nullptr;
Label* m_title = nullptr;
Label* m_detail = nullptr;
InputArea* m_inputArea = nullptr;
Signal<>::ScopedConnection m_paletteConn;
};
std::vector<AudioNode> sortedDevices(const std::vector<AudioNode>& devices) {
std::vector<AudioNode> sorted = devices;
std::ranges::sort(sorted, [](const AudioNode& a, const AudioNode& b) {
const std::string& left = !a.description.empty() ? a.description : a.name;
const std::string& right = !b.description.empty() ? b.description : b.name;
if (left != right) {
return left < right;
}
return a.id < b.id;
});
return sorted;
}
[[nodiscard]] bool hovered() const noexcept { return m_inputArea != nullptr && m_inputArea->hovered(); }
[[nodiscard]] bool pressed() const noexcept { return m_inputArea != nullptr && m_inputArea->pressed(); }
std::function<void()> m_onSelect;
RadioButton* m_radio = nullptr;
Label* m_title = nullptr;
Label* m_detail = nullptr;
InputArea* m_inputArea = nullptr;
Signal<>::ScopedConnection m_paletteConn;
};
std::vector<AudioNode> sortedDevices(const std::vector<AudioNode>& devices) {
std::vector<AudioNode> sorted = devices;
std::ranges::sort(sorted, [](const AudioNode& a, const AudioNode& b) {
const std::string& left = !a.description.empty() ? a.description : a.name;
const std::string& right = !b.description.empty() ? b.description : b.name;
if (left != right) {
return left < right;
}
return a.id < b.id;
});
return sorted;
}
void addSubtitle(Flex& parent, const std::string& text, float scale) {
auto label = std::make_unique<Label>();
label->setText(text);
label->setCaptionStyle();
label->setFontSize(Style::fontSizeCaption * scale);
label->setColor(roleColor(ColorRole::OnSurfaceVariant));
parent.addChild(std::move(label));
}
void addEmptyState(Flex& parent, const std::string& title, const std::string& body, float scale) {
auto card = std::make_unique<Flex>();
card->setDirection(FlexDirection::Vertical);
card->setAlign(FlexAlign::Start);
card->setGap(Style::spaceXs * scale);
card->setPadding(Style::spaceMd * scale);
card->setRadius(Style::radiusMd * scale);
card->setBackground(roleColor(ColorRole::Surface));
card->setBorderWidth(0.0f);
auto titleLabel = std::make_unique<Label>();
titleLabel->setText(title);
titleLabel->setBold(true);
titleLabel->setFontSize(Style::fontSizeBody * scale);
titleLabel->setColor(roleColor(ColorRole::OnSurface));
card->addChild(std::move(titleLabel));
auto bodyLabel = std::make_unique<Label>();
bodyLabel->setText(body);
bodyLabel->setCaptionStyle();
bodyLabel->setFontSize(Style::fontSizeCaption * scale);
bodyLabel->setColor(roleColor(ColorRole::OnSurfaceVariant));
card->addChild(std::move(bodyLabel));
parent.addChild(std::move(card));
}
std::string deviceListKey(const std::vector<AudioNode>& devices) {
std::string key;
for (const auto& device : devices) {
key += std::to_string(device.id);
key.push_back(':');
key += device.isDefault ? '1' : '0';
key.push_back(':');
key += device.name;
key.push_back(':');
key += device.description;
key.push_back('\n');
void addSubtitle(Flex& parent, const std::string& text, float scale) {
auto label = std::make_unique<Label>();
label->setText(text);
label->setCaptionStyle();
label->setFontSize(Style::fontSizeCaption * scale);
label->setColor(roleColor(ColorRole::OnSurfaceVariant));
parent.addChild(std::move(label));
}
return key;
}
std::string widestPercentLabel(float sliderMaxValue) {
const std::size_t digits =
std::to_string(static_cast<int>(std::round(std::max(0.0f, sliderMaxValue) * 100.0f))).size();
return std::string(std::max<std::size_t>(1, digits), '8') + "%";
}
void addEmptyState(Flex& parent, const std::string& title, const std::string& body, float scale) {
auto card = std::make_unique<Flex>();
card->setDirection(FlexDirection::Vertical);
card->setAlign(FlexAlign::Start);
card->setGap(Style::spaceXs * scale);
card->setPadding(Style::spaceMd * scale);
card->setRadius(Style::radiusMd * scale);
card->setBackground(roleColor(ColorRole::Surface));
card->setBorderWidth(0.0f);
auto titleLabel = std::make_unique<Label>();
titleLabel->setText(title);
titleLabel->setBold(true);
titleLabel->setFontSize(Style::fontSizeBody * scale);
titleLabel->setColor(roleColor(ColorRole::OnSurface));
card->addChild(std::move(titleLabel));
auto bodyLabel = std::make_unique<Label>();
bodyLabel->setText(body);
bodyLabel->setCaptionStyle();
bodyLabel->setFontSize(Style::fontSizeCaption * scale);
bodyLabel->setColor(roleColor(ColorRole::OnSurfaceVariant));
card->addChild(std::move(bodyLabel));
parent.addChild(std::move(card));
}
std::string deviceListKey(const std::vector<AudioNode>& devices) {
std::string key;
for (const auto& device : devices) {
key += std::to_string(device.id);
key.push_back(':');
key += device.isDefault ? '1' : '0';
key.push_back(':');
key += device.name;
key.push_back(':');
key += device.description;
key.push_back('\n');
}
return key;
}
std::string widestPercentLabel(float sliderMaxValue) {
const std::size_t digits =
std::to_string(static_cast<int>(std::round(std::max(0.0f, sliderMaxValue) * 100.0f))).size();
return std::string(std::max<std::size_t>(1, digits), '8') + "%";
}
} // namespace
AudioTab::AudioTab(PipeWireService* audio, ConfigService* config) : m_audio(audio), m_config(config) {}
bool AudioTab::dragging() const noexcept {
return (m_outputSlider != nullptr && m_outputSlider->dragging()) || (m_inputSlider != nullptr && m_inputSlider->dragging());
return (m_outputSlider != nullptr && m_outputSlider->dragging()) ||
(m_inputSlider != nullptr && m_inputSlider->dragging());
}
std::unique_ptr<Flex> AudioTab::create() {
@@ -406,12 +406,12 @@ void AudioTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeight
m_rootLayout->layout(renderer);
if (m_outputDeviceLabel != nullptr && m_outputVolumeCard != nullptr) {
m_outputDeviceLabel->setMaxWidth(
std::max(0.0f, m_outputVolumeCard->width() - m_outputVolumeCard->paddingLeft() - m_outputVolumeCard->paddingRight()));
m_outputDeviceLabel->setMaxWidth(std::max(0.0f, m_outputVolumeCard->width() - m_outputVolumeCard->paddingLeft() -
m_outputVolumeCard->paddingRight()));
}
if (m_inputDeviceLabel != nullptr && m_inputVolumeCard != nullptr) {
m_inputDeviceLabel->setMaxWidth(
std::max(0.0f, m_inputVolumeCard->width() - m_inputVolumeCard->paddingLeft() - m_inputVolumeCard->paddingRight()));
m_inputDeviceLabel->setMaxWidth(std::max(0.0f, m_inputVolumeCard->width() - m_inputVolumeCard->paddingLeft() -
m_inputVolumeCard->paddingRight()));
}
m_rootLayout->layout(renderer);
@@ -441,12 +441,12 @@ void AudioTab::doUpdate(Renderer& renderer) {
const bool inputDragging = m_inputSlider != nullptr && m_inputSlider->dragging();
if (m_outputDeviceLabel != nullptr) {
m_outputDeviceLabel->setText(
sink != nullptr ? (!sink->description.empty() ? sink->description : sink->name) : "No output device selected");
m_outputDeviceLabel->setText(sink != nullptr ? (!sink->description.empty() ? sink->description : sink->name)
: "No output device selected");
}
if (m_inputDeviceLabel != nullptr) {
m_inputDeviceLabel->setText(
source != nullptr ? (!source->description.empty() ? source->description : source->name) : "No input device selected");
m_inputDeviceLabel->setText(source != nullptr ? (!source->description.empty() ? source->description : source->name)
: "No input device selected");
}
const float sinkVolume = sink != nullptr ? sink->volume : 0.0f;
@@ -454,17 +454,14 @@ void AudioTab::doUpdate(Renderer& renderer) {
const bool showPendingSink = sink != nullptr && m_pendingSinkVolume >= 0.0f && m_pendingSinkId == sink->id;
const bool showPendingSource = source != nullptr && m_pendingSourceVolume >= 0.0f && m_pendingSourceId == source->id;
const bool holdSinkState = outputDragging && sink != nullptr && m_lastSentSinkVolume >= 0.0f &&
now < m_ignoreSinkStateUntil &&
std::abs(sink->volume - m_lastSentSinkVolume) > 0.02f;
now < m_ignoreSinkStateUntil && std::abs(sink->volume - m_lastSentSinkVolume) > 0.02f;
const bool holdSourceState = inputDragging && source != nullptr && m_lastSentSourceVolume >= 0.0f &&
now < m_ignoreSourceStateUntil &&
std::abs(source->volume - m_lastSentSourceVolume) > 0.02f;
const float displayedSinkVolume =
std::clamp(showPendingSink ? m_pendingSinkVolume : (holdSinkState ? m_lastSentSinkVolume : sinkVolume), 0.0f,
sliderMax);
const float displayedSinkVolume = std::clamp(
showPendingSink ? m_pendingSinkVolume : (holdSinkState ? m_lastSentSinkVolume : sinkVolume), 0.0f, sliderMax);
const float displayedSourceVolume =
std::clamp(showPendingSource ? m_pendingSourceVolume
: (holdSourceState ? m_lastSentSourceVolume : sourceVolume),
std::clamp(showPendingSource ? m_pendingSourceVolume : (holdSourceState ? m_lastSentSourceVolume : sourceVolume),
0.0f, sliderMax);
if (m_outputSlider != nullptr) {
@@ -683,9 +680,8 @@ void AudioTab::flushPendingVolumes(bool force) {
if (shouldSendSink && !force && outputDragging) {
const auto nextSendAt = m_lastSinkCommitAt + kVolumeCommitInterval;
if (now < nextSendAt) {
m_sinkVolumeDebounceTimer.start(
std::chrono::duration_cast<std::chrono::milliseconds>(nextSendAt - now),
[this]() { flushPendingVolumes(); });
m_sinkVolumeDebounceTimer.start(std::chrono::duration_cast<std::chrono::milliseconds>(nextSendAt - now),
[this]() { flushPendingVolumes(); });
shouldSendSink = false;
}
}
@@ -713,9 +709,8 @@ void AudioTab::flushPendingVolumes(bool force) {
if (shouldSendSource && !force && inputDragging) {
const auto nextSendAt = m_lastSourceCommitAt + kVolumeCommitInterval;
if (now < nextSendAt) {
m_sourceVolumeDebounceTimer.start(
std::chrono::duration_cast<std::chrono::milliseconds>(nextSendAt - now),
[this]() { flushPendingVolumes(); });
m_sourceVolumeDebounceTimer.start(std::chrono::duration_cast<std::chrono::milliseconds>(nextSendAt - now),
[this]() { flushPendingVolumes(); });
shouldSendSource = false;
}
}
+1 -1
View File
@@ -3,8 +3,8 @@
#include "core/timer_manager.h"
#include "shell/control_center/tab.h"
#include <cstdint>
#include <chrono>
#include <cstdint>
#include <string>
#include <vector>
+145 -146
View File
@@ -24,167 +24,167 @@ using namespace control_center;
namespace {
constexpr float kRowMinHeight = Style::controlHeightLg;
constexpr float kRowMinHeight = Style::controlHeightLg;
const char* glyphFor(BluetoothDeviceKind kind) {
switch (kind) {
case BluetoothDeviceKind::Headset:
return "bluetooth-device-headset";
case BluetoothDeviceKind::Headphones:
return "bluetooth-device-headphones";
case BluetoothDeviceKind::Earbuds:
return "bluetooth-device-earbuds";
case BluetoothDeviceKind::Speaker:
return "bluetooth-device-speaker";
case BluetoothDeviceKind::Microphone:
return "bluetooth-device-microphone";
case BluetoothDeviceKind::Mouse:
return "bluetooth-device-mouse";
case BluetoothDeviceKind::Keyboard:
return "bluetooth-device-keyboard";
case BluetoothDeviceKind::Phone:
return "bluetooth-device-phone";
case BluetoothDeviceKind::Computer:
return "settings-display";
case BluetoothDeviceKind::Gamepad:
return "bluetooth-device-gamepad";
case BluetoothDeviceKind::Watch:
return "bluetooth-device-watch";
case BluetoothDeviceKind::Tv:
return "bluetooth-device-tv";
case BluetoothDeviceKind::Unknown:
default:
return "bluetooth-device-generic";
}
}
enum class DeviceBucket : std::uint8_t {
Connected,
Paired,
Available,
};
DeviceBucket bucketFor(const BluetoothDeviceInfo& d) {
if (d.connected) {
return DeviceBucket::Connected;
}
if (d.paired) {
return DeviceBucket::Paired;
}
return DeviceBucket::Available;
}
class BluetoothDeviceRow : public Flex {
public:
BluetoothDeviceRow(BluetoothDeviceInfo device, BluetoothService* service, float scale)
: m_device(std::move(device)), m_service(service) {
setDirection(FlexDirection::Horizontal);
setAlign(FlexAlign::Center);
setGap(Style::spaceSm * scale);
setPadding(Style::spaceSm * scale, Style::spaceMd * scale);
setMinHeight(kRowMinHeight * scale);
setRadius(Style::radiusMd * scale);
setBackground(roleColor(ColorRole::Surface));
setBorderWidth(0.0f);
auto icon = std::make_unique<Glyph>();
icon->setGlyph(glyphFor(m_device.kind));
icon->setGlyphSize(Style::fontSizeBody * scale);
icon->setColor(roleColor(ColorRole::OnSurface));
addChild(std::move(icon));
auto alias = std::make_unique<Label>();
alias->setText(m_device.alias);
alias->setBold(m_device.connected);
alias->setFontSize(Style::fontSizeBody * scale);
alias->setColor(roleColor(ColorRole::OnSurface));
alias->setFlexGrow(1.0f);
m_title = alias.get();
addChild(std::move(alias));
if (m_device.hasBattery) {
auto battery = std::make_unique<Label>();
battery->setText(std::to_string(static_cast<int>(m_device.batteryPercent)) + "%");
battery->setCaptionStyle();
battery->setFontSize(Style::fontSizeCaption * scale);
battery->setColor(roleColor(ColorRole::OnSurfaceVariant));
addChild(std::move(battery));
} else if (m_device.hasRssi && bucketFor(m_device) == DeviceBucket::Available) {
auto rssi = std::make_unique<Label>();
rssi->setText(std::to_string(static_cast<int>(m_device.rssi)) + " dBm");
rssi->setCaptionStyle();
rssi->setFontSize(Style::fontSizeCaption * scale);
rssi->setColor(roleColor(ColorRole::OnSurfaceVariant));
addChild(std::move(rssi));
const char* glyphFor(BluetoothDeviceKind kind) {
switch (kind) {
case BluetoothDeviceKind::Headset:
return "bluetooth-device-headset";
case BluetoothDeviceKind::Headphones:
return "bluetooth-device-headphones";
case BluetoothDeviceKind::Earbuds:
return "bluetooth-device-earbuds";
case BluetoothDeviceKind::Speaker:
return "bluetooth-device-speaker";
case BluetoothDeviceKind::Microphone:
return "bluetooth-device-microphone";
case BluetoothDeviceKind::Mouse:
return "bluetooth-device-mouse";
case BluetoothDeviceKind::Keyboard:
return "bluetooth-device-keyboard";
case BluetoothDeviceKind::Phone:
return "bluetooth-device-phone";
case BluetoothDeviceKind::Computer:
return "settings-display";
case BluetoothDeviceKind::Gamepad:
return "bluetooth-device-gamepad";
case BluetoothDeviceKind::Watch:
return "bluetooth-device-watch";
case BluetoothDeviceKind::Tv:
return "bluetooth-device-tv";
case BluetoothDeviceKind::Unknown:
default:
return "bluetooth-device-generic";
}
}
if (m_device.paired) {
auto trust = std::make_unique<Toggle>();
trust->setToggleSize(ToggleSize::Small);
trust->setScale(scale);
trust->setChecked(m_device.trusted);
trust->setOnChange([this](bool checked) {
if (m_service != nullptr) {
m_service->setTrusted(m_device.path, checked);
}
});
addChild(std::move(trust));
}
enum class DeviceBucket : std::uint8_t {
Connected,
Paired,
Available,
};
auto primary = std::make_unique<Button>();
primary->setVariant(ButtonVariant::Default);
switch (bucketFor(m_device)) {
case DeviceBucket::Connected:
primary->setText("Disconnect");
primary->setVariant(ButtonVariant::Outline);
break;
case DeviceBucket::Paired:
primary->setText(m_device.connecting ? "Connecting…" : "Connect");
break;
case DeviceBucket::Available:
primary->setText(m_device.connecting ? "Pairing…" : "Pair");
break;
DeviceBucket bucketFor(const BluetoothDeviceInfo& d) {
if (d.connected) {
return DeviceBucket::Connected;
}
primary->setOnClick([this]() {
if (m_service == nullptr) {
return;
if (d.paired) {
return DeviceBucket::Paired;
}
return DeviceBucket::Available;
}
class BluetoothDeviceRow : public Flex {
public:
BluetoothDeviceRow(BluetoothDeviceInfo device, BluetoothService* service, float scale)
: m_device(std::move(device)), m_service(service) {
setDirection(FlexDirection::Horizontal);
setAlign(FlexAlign::Center);
setGap(Style::spaceSm * scale);
setPadding(Style::spaceSm * scale, Style::spaceMd * scale);
setMinHeight(kRowMinHeight * scale);
setRadius(Style::radiusMd * scale);
setBackground(roleColor(ColorRole::Surface));
setBorderWidth(0.0f);
auto icon = std::make_unique<Glyph>();
icon->setGlyph(glyphFor(m_device.kind));
icon->setGlyphSize(Style::fontSizeBody * scale);
icon->setColor(roleColor(ColorRole::OnSurface));
addChild(std::move(icon));
auto alias = std::make_unique<Label>();
alias->setText(m_device.alias);
alias->setBold(m_device.connected);
alias->setFontSize(Style::fontSizeBody * scale);
alias->setColor(roleColor(ColorRole::OnSurface));
alias->setFlexGrow(1.0f);
m_title = alias.get();
addChild(std::move(alias));
if (m_device.hasBattery) {
auto battery = std::make_unique<Label>();
battery->setText(std::to_string(static_cast<int>(m_device.batteryPercent)) + "%");
battery->setCaptionStyle();
battery->setFontSize(Style::fontSizeCaption * scale);
battery->setColor(roleColor(ColorRole::OnSurfaceVariant));
addChild(std::move(battery));
} else if (m_device.hasRssi && bucketFor(m_device) == DeviceBucket::Available) {
auto rssi = std::make_unique<Label>();
rssi->setText(std::to_string(static_cast<int>(m_device.rssi)) + " dBm");
rssi->setCaptionStyle();
rssi->setFontSize(Style::fontSizeCaption * scale);
rssi->setColor(roleColor(ColorRole::OnSurfaceVariant));
addChild(std::move(rssi));
}
if (m_device.paired) {
auto trust = std::make_unique<Toggle>();
trust->setToggleSize(ToggleSize::Small);
trust->setScale(scale);
trust->setChecked(m_device.trusted);
trust->setOnChange([this](bool checked) {
if (m_service != nullptr) {
m_service->setTrusted(m_device.path, checked);
}
});
addChild(std::move(trust));
}
auto primary = std::make_unique<Button>();
primary->setVariant(ButtonVariant::Default);
switch (bucketFor(m_device)) {
case DeviceBucket::Connected:
m_service->disconnectDevice(m_device.path);
primary->setText("Disconnect");
primary->setVariant(ButtonVariant::Outline);
break;
case DeviceBucket::Paired:
m_service->connect(m_device.path);
primary->setText(m_device.connecting ? "Connecting…" : "Connect");
break;
case DeviceBucket::Available:
m_service->pair(m_device.path);
primary->setText(m_device.connecting ? "Pairing…" : "Pair");
break;
}
PanelManager::instance().refresh();
});
m_primaryButton = static_cast<Button*>(addChild(std::move(primary)));
if (m_device.paired) {
auto forget = std::make_unique<Button>();
forget->setVariant(ButtonVariant::Ghost);
forget->setText("Forget");
forget->setOnClick([this]() {
if (m_service != nullptr) {
m_service->forget(m_device.path);
primary->setOnClick([this]() {
if (m_service == nullptr) {
return;
}
switch (bucketFor(m_device)) {
case DeviceBucket::Connected:
m_service->disconnectDevice(m_device.path);
break;
case DeviceBucket::Paired:
m_service->connect(m_device.path);
break;
case DeviceBucket::Available:
m_service->pair(m_device.path);
break;
}
PanelManager::instance().refresh();
});
m_forgetButton = static_cast<Button*>(addChild(std::move(forget)));
}
}
m_primaryButton = static_cast<Button*>(addChild(std::move(primary)));
private:
BluetoothDeviceInfo m_device;
BluetoothService* m_service = nullptr;
Label* m_title = nullptr;
Button* m_primaryButton = nullptr;
Button* m_forgetButton = nullptr;
};
if (m_device.paired) {
auto forget = std::make_unique<Button>();
forget->setVariant(ButtonVariant::Ghost);
forget->setText("Forget");
forget->setOnClick([this]() {
if (m_service != nullptr) {
m_service->forget(m_device.path);
}
PanelManager::instance().refresh();
});
m_forgetButton = static_cast<Button*>(addChild(std::move(forget)));
}
}
private:
BluetoothDeviceInfo m_device;
BluetoothService* m_service = nullptr;
Label* m_title = nullptr;
Button* m_primaryButton = nullptr;
Button* m_forgetButton = nullptr;
};
} // namespace
@@ -502,8 +502,7 @@ void BluetoothTab::syncPairingCard() {
if (m_pairingTitle != nullptr) {
m_pairingTitle->setText("Pair " + alias);
}
const bool needsInput =
req.kind == BluetoothPairingKind::PinCode || req.kind == BluetoothPairingKind::Passkey;
const bool needsInput = req.kind == BluetoothPairingKind::PinCode || req.kind == BluetoothPairingKind::Passkey;
const bool showsCode = req.kind == BluetoothPairingKind::Confirm ||
req.kind == BluetoothPairingKind::DisplayPasskey ||
req.kind == BluetoothPairingKind::DisplayPinCode;
@@ -634,7 +633,7 @@ void BluetoothTab::rebuildDeviceList(Renderer& renderer) {
if (devices.empty()) {
auto empty = std::make_unique<Label>();
empty->setText(m_service->state().powered ? "No devices found. Start scanning to discover nearby Bluetooth devices."
: "Bluetooth is off");
: "Bluetooth is off");
empty->setCaptionStyle();
empty->setFontSize(Style::fontSizeCaption);
empty->setColor(roleColor(ColorRole::OnSurfaceVariant));
+61 -61
View File
@@ -1,6 +1,6 @@
#include "core/ui_phase.h"
#include "shell/control_center/calendar_tab.h"
#include "core/ui_phase.h"
#include "render/core/renderer.h"
#include "shell/control_center/tab.h"
#include "shell/panel/panel_manager.h"
@@ -20,73 +20,73 @@
namespace {
constexpr float kCalendarGridGap = Style::spaceSm;
constexpr float kCalendarNavButtonSize = Style::controlHeight;
constexpr float kCalendarWeekdayRowHeight = Style::controlHeight;
constexpr float kCalendarHeaderHeight = Style::controlHeightLg;
constexpr float kCalendarCellSizeMin = Style::controlHeightSm + Style::spaceXs;
constexpr float kCalendarCellSizeMax = Style::controlHeightLg + Style::spaceLg;
constexpr float kCalendarLayoutEpsilon = 0.5f;
constexpr float kCalendarGridGap = Style::spaceSm;
constexpr float kCalendarNavButtonSize = Style::controlHeight;
constexpr float kCalendarWeekdayRowHeight = Style::controlHeight;
constexpr float kCalendarHeaderHeight = Style::controlHeightLg;
constexpr float kCalendarCellSizeMin = Style::controlHeightSm + Style::spaceXs;
constexpr float kCalendarCellSizeMax = Style::controlHeightLg + Style::spaceLg;
constexpr float kCalendarLayoutEpsilon = 0.5f;
std::string todayLabel() {
std::time_t now = std::time(nullptr);
std::tm local = *std::localtime(&now);
char buf[64];
if (std::strftime(buf, sizeof(buf), "Today · %A, %d %B %Y", &local) == 0) {
return "Today";
std::string todayLabel() {
std::time_t now = std::time(nullptr);
std::tm local = *std::localtime(&now);
char buf[64];
if (std::strftime(buf, sizeof(buf), "Today · %A, %d %B %Y", &local) == 0) {
return "Today";
}
return buf;
}
return buf;
}
std::string monthName(int month) {
static constexpr std::array<const char*, 12> kMonths = {"January", "February", "March", "April",
"May", "June", "July", "August",
"September", "October", "November", "December"};
if (month < 0 || month >= static_cast<int>(kMonths.size())) {
return "Calendar";
std::string monthName(int month) {
static constexpr std::array<const char*, 12> kMonths = {"January", "February", "March", "April",
"May", "June", "July", "August",
"September", "October", "November", "December"};
if (month < 0 || month >= static_cast<int>(kMonths.size())) {
return "Calendar";
}
return kMonths[static_cast<std::size_t>(month)];
}
return kMonths[static_cast<std::size_t>(month)];
}
int daysInMonth(int yearValue, int monthValue) {
static constexpr std::array<int, 12> kDays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int result = kDays[static_cast<std::size_t>(monthValue)];
if (monthValue == 1) {
const bool leap = ((yearValue % 4 == 0 && yearValue % 100 != 0) || (yearValue % 400 == 0));
result = leap ? 29 : 28;
int daysInMonth(int yearValue, int monthValue) {
static constexpr std::array<int, 12> kDays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int result = kDays[static_cast<std::size_t>(monthValue)];
if (monthValue == 1) {
const bool leap = ((yearValue % 4 == 0 && yearValue % 100 != 0) || (yearValue % 400 == 0));
result = leap ? 29 : 28;
}
return result;
}
return result;
}
struct CalendarBuildState {
int currentYear = 0;
int currentMonth = 0;
int today = 0;
int displayYear = 0;
int displayMonth = 0;
int displayWeekday = 0;
bool isCurrentMonth = false;
};
struct CalendarBuildState {
int currentYear = 0;
int currentMonth = 0;
int today = 0;
int displayYear = 0;
int displayMonth = 0;
int displayWeekday = 0;
bool isCurrentMonth = false;
};
CalendarBuildState currentCalendarState(int monthOffset) {
std::time_t now = std::time(nullptr);
std::tm local = *std::localtime(&now);
CalendarBuildState currentCalendarState(int monthOffset) {
std::time_t now = std::time(nullptr);
std::tm local = *std::localtime(&now);
CalendarBuildState state;
state.currentYear = local.tm_year + 1900;
state.currentMonth = local.tm_mon;
state.today = local.tm_mday;
CalendarBuildState state;
state.currentYear = local.tm_year + 1900;
state.currentMonth = local.tm_mon;
state.today = local.tm_mday;
std::tm display = local;
display.tm_mday = 1;
display.tm_mon += monthOffset;
std::mktime(&display);
state.displayYear = display.tm_year + 1900;
state.displayMonth = display.tm_mon;
state.displayWeekday = display.tm_wday;
state.isCurrentMonth = state.displayYear == state.currentYear && state.displayMonth == state.currentMonth;
return state;
}
std::tm display = local;
display.tm_mday = 1;
display.tm_mon += monthOffset;
std::mktime(&display);
state.displayYear = display.tm_year + 1900;
state.displayMonth = display.tm_mon;
state.displayWeekday = display.tm_wday;
state.isCurrentMonth = state.displayYear == state.currentYear && state.displayMonth == state.currentMonth;
return state;
}
} // namespace
@@ -225,8 +225,8 @@ void CalendarTab::doLayout(Renderer& renderer, float contentWidth, float bodyHei
const bool sizeChanged = std::abs(innerWidth - m_lastInnerWidth) >= kCalendarLayoutEpsilon ||
std::abs(innerHeight - m_lastInnerHeight) >= kCalendarLayoutEpsilon;
const bool displayChanged = state.displayYear != m_lastDisplayYear || state.displayMonth != m_lastDisplayMonth;
const bool todayChanged = state.currentYear != m_lastCurrentYear || state.currentMonth != m_lastCurrentMonth ||
state.today != m_lastToday;
const bool todayChanged =
state.currentYear != m_lastCurrentYear || state.currentMonth != m_lastCurrentMonth || state.today != m_lastToday;
if (!sizeChanged && !displayChanged && !todayChanged) {
return;
}
@@ -359,7 +359,7 @@ void CalendarTab::rebuild() {
cell->setMinHeight(dayCellHeight);
cell->setRadius(Style::radiusLg * scale);
cell->setFontSize(dayCellHeight > (Style::controlHeightLg + Style::spaceXs) * scale ? Style::fontSizeTitle * scale
: Style::fontSizeBody * scale);
: Style::fontSizeBody * scale);
cell->setText("");
if (index < firstWeekdayMonBased) {
@@ -2,8 +2,8 @@
#include "shell/control_center/audio_tab.h"
#include "shell/control_center/bluetooth_tab.h"
#include "shell/control_center/display_tab.h"
#include "shell/control_center/calendar_tab.h"
#include "shell/control_center/display_tab.h"
#include "shell/control_center/media_tab.h"
#include "shell/control_center/network_tab.h"
#include "shell/control_center/notifications_tab.h"
@@ -38,11 +38,12 @@ class WeatherService;
class ControlCenterPanel : public Panel {
public:
ControlCenterPanel(NotificationManager* notifications, PipeWireService* audio, MprisService* mpris,
ConfigService* config = nullptr, HttpClient* httpClient = nullptr, WeatherService* weather = nullptr,
PipeWireSpectrum* spectrum = nullptr, UPowerService* upower = nullptr,
PowerProfilesService* powerProfiles = nullptr, NetworkService* network = nullptr,
NetworkSecretAgent* networkSecrets = nullptr, BluetoothService* bluetooth = nullptr,
BluetoothAgent* bluetoothAgent = nullptr, BrightnessService* brightness = nullptr);
ConfigService* config = nullptr, HttpClient* httpClient = nullptr,
WeatherService* weather = nullptr, PipeWireSpectrum* spectrum = nullptr,
UPowerService* upower = nullptr, PowerProfilesService* powerProfiles = nullptr,
NetworkService* network = nullptr, NetworkSecretAgent* networkSecrets = nullptr,
BluetoothService* bluetooth = nullptr, BluetoothAgent* bluetoothAgent = nullptr,
BrightnessService* brightness = nullptr);
void create() override;
void onFrameTick(float deltaMs) override;
+13 -15
View File
@@ -18,18 +18,18 @@ using namespace control_center;
namespace {
constexpr float kBrightnessSyncEpsilon = 0.005f;
constexpr auto kBrightnessCommitInterval = std::chrono::milliseconds(16);
constexpr auto kBrightnessStateHoldoff = std::chrono::milliseconds(180);
constexpr float kBrightnessSyncEpsilon = 0.005f;
constexpr auto kBrightnessCommitInterval = std::chrono::milliseconds(16);
constexpr auto kBrightnessStateHoldoff = std::chrono::milliseconds(180);
std::string buildDisplayListKey(const std::vector<BrightnessDisplay>& displays) {
std::string key;
for (const auto& d : displays) {
key += d.id;
key += ';';
std::string buildDisplayListKey(const std::vector<BrightnessDisplay>& displays) {
std::string key;
for (const auto& d : displays) {
key += d.id;
key += ';';
}
return key;
}
return key;
}
} // namespace
@@ -126,8 +126,8 @@ void DisplayTab::doUpdate(Renderer& renderer) {
const bool holdState = isDragging && m_lastSentBrightness >= 0.0f && now < m_ignoreStateUntil &&
std::abs(display->brightness - m_lastSentBrightness) > 0.02f;
const float displayedBrightness =
std::clamp(isPending ? m_pendingBrightness : (holdState ? m_lastSentBrightness : display->brightness), 0.0f, 1.0f);
const float displayedBrightness = std::clamp(
isPending ? m_pendingBrightness : (holdState ? m_lastSentBrightness : display->brightness), 0.0f, 1.0f);
card.slider->setEnabled(true);
if (!isDragging && std::abs(displayedBrightness - card.lastBrightness) >= kBrightnessSyncEpsilon) {
@@ -286,9 +286,7 @@ void DisplayTab::queueBrightness(const std::string& displayId, float value) {
}
if (!m_debounceTimer.active()) {
m_debounceTimer.start(kBrightnessCommitInterval, [this]() {
flushPendingBrightness();
});
m_debounceTimer.start(kBrightnessCommitInterval, [this]() { flushPendingBrightness(); });
}
}
+134 -135
View File
@@ -31,156 +31,156 @@ using namespace control_center;
namespace {
const Logger kLog{"media_tab"};
const Logger kLog{"media_tab"};
constexpr float kArtworkSize = Style::controlHeightLg * 6;
constexpr float kMediaNowCardMinHeight = Style::controlHeightLg * 11 + Style::spaceSm * 2;
constexpr float kMediaControlsHeight = Style::controlHeightLg + Style::spaceXs;
constexpr float kMediaPlayPauseHeight = Style::controlHeightLg + Style::spaceSm;
constexpr float kMediaArtworkMinHeight = Style::controlHeightLg * 4;
constexpr auto kNoActivePlayerGrace = std::chrono::milliseconds(2000);
constexpr float kArtworkSize = Style::controlHeightLg * 6;
constexpr float kMediaNowCardMinHeight = Style::controlHeightLg * 11 + Style::spaceSm * 2;
constexpr float kMediaControlsHeight = Style::controlHeightLg + Style::spaceXs;
constexpr float kMediaPlayPauseHeight = Style::controlHeightLg + Style::spaceSm;
constexpr float kMediaArtworkMinHeight = Style::controlHeightLg * 4;
constexpr auto kNoActivePlayerGrace = std::chrono::milliseconds(2000);
bool isRemoteArtUrl(std::string_view artUrl) {
return artUrl.starts_with("https://") || artUrl.starts_with("http://");
}
std::string extractQueryParam(std::string_view url, std::string_view key) {
const auto queryPos = url.find('?');
if (queryPos == std::string_view::npos) {
return {};
bool isRemoteArtUrl(std::string_view artUrl) {
return artUrl.starts_with("https://") || artUrl.starts_with("http://");
}
std::string_view query = url.substr(queryPos + 1);
while (!query.empty()) {
const auto ampPos = query.find('&');
const std::string_view pair = query.substr(0, ampPos);
const auto eqPos = pair.find('=');
const std::string_view pairKey = pair.substr(0, eqPos);
if (pairKey == key) {
return eqPos == std::string_view::npos ? std::string{} : std::string(pair.substr(eqPos + 1));
std::string extractQueryParam(std::string_view url, std::string_view key) {
const auto queryPos = url.find('?');
if (queryPos == std::string_view::npos) {
return {};
}
if (ampPos == std::string_view::npos) {
break;
std::string_view query = url.substr(queryPos + 1);
while (!query.empty()) {
const auto ampPos = query.find('&');
const std::string_view pair = query.substr(0, ampPos);
const auto eqPos = pair.find('=');
const std::string_view pairKey = pair.substr(0, eqPos);
if (pairKey == key) {
return eqPos == std::string_view::npos ? std::string{} : std::string(pair.substr(eqPos + 1));
}
if (ampPos == std::string_view::npos) {
break;
}
query.remove_prefix(ampPos + 1);
}
query.remove_prefix(ampPos + 1);
}
return {};
}
std::string deriveYouTubeThumbnailUrl(std::string_view sourceUrl) {
if (sourceUrl.empty()) {
return {};
}
std::string videoId;
if (sourceUrl.find("youtube.com/watch") != std::string_view::npos) {
videoId = extractQueryParam(sourceUrl, "v");
} else if (sourceUrl.find("youtu.be/") != std::string_view::npos) {
const auto marker = sourceUrl.find("youtu.be/");
const auto start = marker + std::string_view("youtu.be/").size();
const auto end = sourceUrl.find_first_of("?#&/", start);
videoId =
std::string(sourceUrl.substr(start, end == std::string_view::npos ? sourceUrl.size() - start : end - start));
} else if (sourceUrl.find("youtube.com/shorts/") != std::string_view::npos) {
const auto marker = sourceUrl.find("youtube.com/shorts/");
const auto start = marker + std::string_view("youtube.com/shorts/").size();
const auto end = sourceUrl.find_first_of("?#&/", start);
videoId =
std::string(sourceUrl.substr(start, end == std::string_view::npos ? sourceUrl.size() - start : end - start));
std::string deriveYouTubeThumbnailUrl(std::string_view sourceUrl) {
if (sourceUrl.empty()) {
return {};
}
std::string videoId;
if (sourceUrl.find("youtube.com/watch") != std::string_view::npos) {
videoId = extractQueryParam(sourceUrl, "v");
} else if (sourceUrl.find("youtu.be/") != std::string_view::npos) {
const auto marker = sourceUrl.find("youtu.be/");
const auto start = marker + std::string_view("youtu.be/").size();
const auto end = sourceUrl.find_first_of("?#&/", start);
videoId =
std::string(sourceUrl.substr(start, end == std::string_view::npos ? sourceUrl.size() - start : end - start));
} else if (sourceUrl.find("youtube.com/shorts/") != std::string_view::npos) {
const auto marker = sourceUrl.find("youtube.com/shorts/");
const auto start = marker + std::string_view("youtube.com/shorts/").size();
const auto end = sourceUrl.find_first_of("?#&/", start);
videoId =
std::string(sourceUrl.substr(start, end == std::string_view::npos ? sourceUrl.size() - start : end - start));
}
if (videoId.empty()) {
return {};
}
return std::format("https://i.ytimg.com/vi/{}/hqdefault.jpg", videoId);
}
if (videoId.empty()) {
return {};
std::string effectiveArtUrl(const MprisPlayerInfo& player) {
if (!player.artUrl.empty()) {
return player.artUrl;
}
return deriveYouTubeThumbnailUrl(player.sourceUrl);
}
return std::format("https://i.ytimg.com/vi/{}/hqdefault.jpg", videoId);
}
std::string effectiveArtUrl(const MprisPlayerInfo& player) {
if (!player.artUrl.empty()) {
return player.artUrl;
int hexValue(char ch) {
if (ch >= '0' && ch <= '9') {
return ch - '0';
}
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
if (ch >= 'a' && ch <= 'f') {
return 10 + (ch - 'a');
}
return -1;
}
return deriveYouTubeThumbnailUrl(player.sourceUrl);
}
int hexValue(char ch) {
if (ch >= '0' && ch <= '9') {
return ch - '0';
std::string decodeUriComponent(std::string_view text) {
std::string decoded;
decoded.reserve(text.size());
for (std::size_t i = 0; i < text.size(); ++i) {
if (text[i] == '%' && i + 2 < text.size()) {
const int hi = hexValue(text[i + 1]);
const int lo = hexValue(text[i + 2]);
if (hi >= 0 && lo >= 0) {
decoded.push_back(static_cast<char>((hi << 4) | lo));
i += 2;
continue;
}
}
decoded.push_back(text[i]);
}
return decoded;
}
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
if (ch >= 'a' && ch <= 'f') {
return 10 + (ch - 'a');
}
return -1;
}
std::string decodeUriComponent(std::string_view text) {
std::string decoded;
decoded.reserve(text.size());
for (std::size_t i = 0; i < text.size(); ++i) {
if (text[i] == '%' && i + 2 < text.size()) {
const int hi = hexValue(text[i + 1]);
const int lo = hexValue(text[i + 2]);
if (hi >= 0 && lo >= 0) {
decoded.push_back(static_cast<char>((hi << 4) | lo));
i += 2;
continue;
std::string normalizeArtPath(std::string_view artUrl) {
if (artUrl.empty()) {
return {};
}
if (isRemoteArtUrl(artUrl)) {
return {};
}
std::string path(artUrl);
constexpr std::string_view prefix = "file://";
if (path.starts_with(prefix)) {
path.erase(0, prefix.size());
if (path.starts_with("localhost/")) {
path.erase(0, std::string_view("localhost").size());
} else if (!path.empty() && path.front() != '/') {
const auto firstSlash = path.find('/');
path = firstSlash == std::string::npos ? std::string{} : path.substr(firstSlash);
}
}
decoded.push_back(text[i]);
return decodeUriComponent(path);
}
return decoded;
}
std::filesystem::path artCachePath(std::string_view artUrl) {
const std::filesystem::path cacheDir = std::filesystem::path("/tmp") / "noctalia-media-art";
const std::size_t hash = std::hash<std::string_view>{}(artUrl);
return cacheDir / (std::to_string(hash) + ".img");
}
std::string normalizeArtPath(std::string_view artUrl) {
if (artUrl.empty()) {
return {};
}
if (isRemoteArtUrl(artUrl)) {
return {};
}
std::string path(artUrl);
constexpr std::string_view prefix = "file://";
if (path.starts_with(prefix)) {
path.erase(0, prefix.size());
if (path.starts_with("localhost/")) {
path.erase(0, std::string_view("localhost").size());
} else if (!path.empty() && path.front() != '/') {
const auto firstSlash = path.find('/');
path = firstSlash == std::string::npos ? std::string{} : path.substr(firstSlash);
std::string joinArtists(const std::vector<std::string>& artists) {
if (artists.empty()) {
return {};
}
std::string joined = artists.front();
for (std::size_t i = 1; i < artists.size(); ++i) {
joined += ", ";
joined += artists[i];
}
return joined;
}
return decodeUriComponent(path);
}
std::filesystem::path artCachePath(std::string_view artUrl) {
const std::filesystem::path cacheDir = std::filesystem::path("/tmp") / "noctalia-media-art";
const std::size_t hash = std::hash<std::string_view>{}(artUrl);
return cacheDir / (std::to_string(hash) + ".img");
}
std::string joinArtists(const std::vector<std::string>& artists) {
if (artists.empty()) {
return {};
std::string playPauseGlyph(const std::string& playbackStatus) {
return playbackStatus == "Playing" ? "media-pause" : "media-play";
}
std::string joined = artists.front();
for (std::size_t i = 1; i < artists.size(); ++i) {
joined += ", ";
joined += artists[i];
}
return joined;
}
std::string playPauseGlyph(const std::string& playbackStatus) {
return playbackStatus == "Playing" ? "media-pause" : "media-play";
}
std::string repeatGlyph(const std::string& loopStatus) { return loopStatus == "Track" ? "repeat-once" : "repeat"; }
std::string repeatGlyph(const std::string& loopStatus) { return loopStatus == "Track" ? "repeat-once" : "repeat"; }
ButtonVariant toggleVariant(bool active) { return active ? ButtonVariant::Accent : ButtonVariant::Ghost; }
ButtonVariant toggleVariant(bool active) { return active ? ButtonVariant::Accent : ButtonVariant::Ghost; }
} // namespace
@@ -607,8 +607,9 @@ void MediaTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeight
}
if (m_playerMenu != nullptr && m_nowCard != nullptr) {
const float menuWidth = std::clamp(Style::controlHeightLg * 6.0f * scale, Style::controlHeightLg * 4.2f * scale,
std::max(1.0f, m_nowCard->width() - (m_nowCard->paddingLeft() + m_nowCard->paddingRight())));
const float menuWidth =
std::clamp(Style::controlHeightLg * 6.0f * scale, Style::controlHeightLg * 4.2f * scale,
std::max(1.0f, m_nowCard->width() - (m_nowCard->paddingLeft() + m_nowCard->paddingRight())));
m_playerMenu->setMenuWidth(menuWidth);
m_playerMenu->setVisible(m_playerMenuOpen);
if (m_playerMenuOpen) {
@@ -621,8 +622,8 @@ void MediaTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeight
Node::absolutePosition(m_rootLayout, rootAbsX, rootAbsY);
const float localNowX = nowAbsX - rootAbsX;
const float localNowY = nowAbsY - rootAbsY;
const float x = localNowX + std::max(m_nowCard->paddingLeft(),
m_nowCard->width() - m_nowCard->paddingRight() - menuWidth);
const float x =
localNowX + std::max(m_nowCard->paddingLeft(), m_nowCard->width() - m_nowCard->paddingRight() - menuWidth);
const float y = localNowY + m_nowCard->paddingTop() + Style::controlHeightSm * scale + Style::spaceXs * scale;
m_playerMenu->setPosition(x, y);
m_playerMenu->layout(renderer);
@@ -630,10 +631,10 @@ void MediaTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeight
}
if (m_visualizerBody != nullptr && m_visualizerSpectrum != nullptr) {
const float bodyWidth =
std::max(0.0f, m_visualizerBody->width() - (m_visualizerBody->paddingLeft() + m_visualizerBody->paddingRight()));
const float bodyHeightAvail =
std::max(0.0f, m_visualizerBody->height() - (m_visualizerBody->paddingTop() + m_visualizerBody->paddingBottom()));
const float bodyWidth = std::max(0.0f, m_visualizerBody->width() -
(m_visualizerBody->paddingLeft() + m_visualizerBody->paddingRight()));
const float bodyHeightAvail = std::max(
0.0f, m_visualizerBody->height() - (m_visualizerBody->paddingTop() + m_visualizerBody->paddingBottom()));
const float spectrumWidth = std::max(1.0f, bodyWidth);
const float spectrumHeight = std::max(1.0f, bodyHeightAvail);
m_visualizerSpectrum->setSize(spectrumWidth, spectrumHeight);
@@ -759,8 +760,7 @@ void MediaTab::refresh(Renderer& renderer) {
}
}
if (!active.has_value() && m_lastActiveSnapshot.has_value() &&
now - m_lastActiveSeenAt <= kNoActivePlayerGrace) {
if (!active.has_value() && m_lastActiveSnapshot.has_value() && now - m_lastActiveSeenAt <= kNoActivePlayerGrace) {
// Keep last player briefly to hide transient MPRIS discovery gaps.
active = m_lastActiveSnapshot;
}
@@ -796,8 +796,8 @@ void MediaTab::refresh(Renderer& renderer) {
}
}
if (m_trackTitle == nullptr || m_trackArtist == nullptr || m_progressSlider == nullptr || m_playPauseButton == nullptr ||
m_repeatButton == nullptr || m_shuffleButton == nullptr) {
if (m_trackTitle == nullptr || m_trackArtist == nullptr || m_progressSlider == nullptr ||
m_playPauseButton == nullptr || m_repeatButton == nullptr || m_shuffleButton == nullptr) {
return;
}
@@ -854,8 +854,7 @@ void MediaTab::refresh(Renderer& renderer) {
std::error_code ec;
if (std::filesystem::exists(cached, ec) && std::filesystem::file_size(cached, ec) > 0) {
artPath = cached.string();
} else if (m_httpClient != nullptr &&
m_pendingArtDownloads.find(resolvedArtUrl) == m_pendingArtDownloads.end()) {
} else if (m_httpClient != nullptr && m_pendingArtDownloads.find(resolvedArtUrl) == m_pendingArtDownloads.end()) {
std::filesystem::create_directories(cached.parent_path(), ec);
m_pendingArtDownloads.insert(resolvedArtUrl);
m_httpClient->download(resolvedArtUrl, cached, [this, url = resolvedArtUrl](bool success) {
+142 -142
View File
@@ -23,169 +23,169 @@ using namespace control_center;
namespace {
constexpr float kRowMinHeight = Style::controlHeightLg;
constexpr float kRowMinHeight = Style::controlHeightLg;
std::string currentTitle(const NetworkState& s) {
if (s.kind == NetworkConnectivity::Wireless && s.connected && !s.ssid.empty()) {
return s.ssid;
}
if (s.kind == NetworkConnectivity::Wired && s.connected) {
return s.interfaceName.empty() ? std::string("Wired connection") : s.interfaceName;
}
return "Not connected";
}
std::string currentDetail(const NetworkState& s) {
if (!s.connected) {
return s.wirelessEnabled ? "Wi-Fi is on" : "Wi-Fi is off";
}
std::string out;
if (!s.interfaceName.empty()) {
out = s.interfaceName;
}
if (!s.ipv4.empty()) {
if (!out.empty()) {
out += "";
std::string currentTitle(const NetworkState& s) {
if (s.kind == NetworkConnectivity::Wireless && s.connected && !s.ssid.empty()) {
return s.ssid;
}
out += s.ipv4;
}
if (s.kind == NetworkConnectivity::Wireless && s.signalStrength > 0) {
if (!out.empty()) {
out += "";
if (s.kind == NetworkConnectivity::Wired && s.connected) {
return s.interfaceName.empty() ? std::string("Wired connection") : s.interfaceName;
}
out += std::to_string(static_cast<int>(s.signalStrength)) + "%";
return "Not connected";
}
return out;
}
class AccessPointRow : public Flex {
public:
AccessPointRow(AccessPointInfo ap, bool saved, std::function<void(const AccessPointInfo&)> onActivate,
std::function<void(const AccessPointInfo&)> onForget)
: m_ap(std::move(ap)), m_onActivate(std::move(onActivate)), m_onForget(std::move(onForget)) {
setDirection(FlexDirection::Horizontal);
setAlign(FlexAlign::Center);
setGap(Style::spaceSm);
setPadding(Style::spaceSm, Style::spaceMd);
setMinHeight(kRowMinHeight);
setRadius(Style::radiusMd);
setBackground(roleColor(ColorRole::Surface));
setBorderWidth(0.0f);
auto signalGlyph = std::make_unique<Glyph>();
signalGlyph->setGlyph(NetworkTab_rowGlyph(m_ap));
signalGlyph->setGlyphSize(Style::fontSizeBody);
signalGlyph->setColor(roleColor(ColorRole::OnSurface));
addChild(std::move(signalGlyph));
auto ssid = std::make_unique<Label>();
ssid->setText(m_ap.ssid);
ssid->setBold(m_ap.active);
ssid->setFontSize(Style::fontSizeBody);
ssid->setColor(roleColor(ColorRole::OnSurface));
ssid->setFlexGrow(1.0f);
m_title = ssid.get();
addChild(std::move(ssid));
if (m_ap.secured) {
auto lock = std::make_unique<Glyph>();
lock->setGlyph("lock");
lock->setGlyphSize(Style::fontSizeCaption);
lock->setColor(roleColor(ColorRole::OnSurfaceVariant));
addChild(std::move(lock));
std::string currentDetail(const NetworkState& s) {
if (!s.connected) {
return s.wirelessEnabled ? "Wi-Fi is on" : "Wi-Fi is off";
}
std::string out;
if (!s.interfaceName.empty()) {
out = s.interfaceName;
}
if (!s.ipv4.empty()) {
if (!out.empty()) {
out += "";
}
out += s.ipv4;
}
if (s.kind == NetworkConnectivity::Wireless && s.signalStrength > 0) {
if (!out.empty()) {
out += "";
}
out += std::to_string(static_cast<int>(s.signalStrength)) + "%";
}
return out;
}
auto strength = std::make_unique<Label>();
strength->setText(std::to_string(static_cast<int>(m_ap.strength)) + "%");
strength->setCaptionStyle();
strength->setFontSize(Style::fontSizeCaption);
strength->setColor(roleColor(ColorRole::OnSurfaceVariant));
addChild(std::move(strength));
class AccessPointRow : public Flex {
public:
AccessPointRow(AccessPointInfo ap, bool saved, std::function<void(const AccessPointInfo&)> onActivate,
std::function<void(const AccessPointInfo&)> onForget)
: m_ap(std::move(ap)), m_onActivate(std::move(onActivate)), m_onForget(std::move(onForget)) {
setDirection(FlexDirection::Horizontal);
setAlign(FlexAlign::Center);
setGap(Style::spaceSm);
setPadding(Style::spaceSm, Style::spaceMd);
setMinHeight(kRowMinHeight);
setRadius(Style::radiusMd);
setBackground(roleColor(ColorRole::Surface));
setBorderWidth(0.0f);
if (saved) {
auto forget = std::make_unique<Button>();
forget->setVariant(ButtonVariant::Ghost);
forget->setText("Forget");
forget->setOnClick([this]() {
if (m_onForget) {
m_onForget(m_ap);
auto signalGlyph = std::make_unique<Glyph>();
signalGlyph->setGlyph(NetworkTab_rowGlyph(m_ap));
signalGlyph->setGlyphSize(Style::fontSizeBody);
signalGlyph->setColor(roleColor(ColorRole::OnSurface));
addChild(std::move(signalGlyph));
auto ssid = std::make_unique<Label>();
ssid->setText(m_ap.ssid);
ssid->setBold(m_ap.active);
ssid->setFontSize(Style::fontSizeBody);
ssid->setColor(roleColor(ColorRole::OnSurface));
ssid->setFlexGrow(1.0f);
m_title = ssid.get();
addChild(std::move(ssid));
if (m_ap.secured) {
auto lock = std::make_unique<Glyph>();
lock->setGlyph("lock");
lock->setGlyphSize(Style::fontSizeCaption);
lock->setColor(roleColor(ColorRole::OnSurfaceVariant));
addChild(std::move(lock));
}
auto strength = std::make_unique<Label>();
strength->setText(std::to_string(static_cast<int>(m_ap.strength)) + "%");
strength->setCaptionStyle();
strength->setFontSize(Style::fontSizeCaption);
strength->setColor(roleColor(ColorRole::OnSurfaceVariant));
addChild(std::move(strength));
if (saved) {
auto forget = std::make_unique<Button>();
forget->setVariant(ButtonVariant::Ghost);
forget->setText("Forget");
forget->setOnClick([this]() {
if (m_onForget) {
m_onForget(m_ap);
}
});
m_forgetButton = static_cast<Button*>(addChild(std::move(forget)));
}
auto area = std::make_unique<InputArea>();
area->setPropagateEvents(true);
area->setOnEnter([this](const InputArea::PointerData& /*data*/) { applyState(); });
area->setOnLeave([this]() { applyState(); });
area->setOnPress([this](const InputArea::PointerData& /*data*/) { applyState(); });
area->setOnClick([this](const InputArea::PointerData& /*data*/) {
if (m_onActivate) {
m_onActivate(m_ap);
}
});
m_forgetButton = static_cast<Button*>(addChild(std::move(forget)));
m_inputArea = static_cast<InputArea*>(addChild(std::move(area)));
applyState();
m_paletteConn = paletteChanged().connect([this] { applyState(); });
}
auto area = std::make_unique<InputArea>();
area->setPropagateEvents(true);
area->setOnEnter([this](const InputArea::PointerData& /*data*/) { applyState(); });
area->setOnLeave([this]() { applyState(); });
area->setOnPress([this](const InputArea::PointerData& /*data*/) { applyState(); });
area->setOnClick([this](const InputArea::PointerData& /*data*/) {
if (m_onActivate) {
m_onActivate(m_ap);
void doLayout(Renderer& renderer) override {
if (m_inputArea == nullptr) {
return;
}
});
m_inputArea = static_cast<InputArea*>(addChild(std::move(area)));
applyState();
m_paletteConn = paletteChanged().connect([this] { applyState(); });
}
void doLayout(Renderer& renderer) override {
if (m_inputArea == nullptr) {
return;
m_inputArea->setVisible(false);
Flex::doLayout(renderer);
m_inputArea->setVisible(true);
m_inputArea->setPosition(0.0f, 0.0f);
m_inputArea->setSize(width(), height());
// Carve out the forget button so its own InputArea gets clicks first.
if (m_forgetButton != nullptr) {
const float areaWidth = std::max(0.0f, m_forgetButton->x() - gap());
m_inputArea->setSize(areaWidth, height());
}
applyState();
}
m_inputArea->setVisible(false);
Flex::doLayout(renderer);
m_inputArea->setVisible(true);
m_inputArea->setPosition(0.0f, 0.0f);
m_inputArea->setSize(width(), height());
// Carve out the forget button so its own InputArea gets clicks first.
if (m_forgetButton != nullptr) {
const float areaWidth = std::max(0.0f, m_forgetButton->x() - gap());
m_inputArea->setSize(areaWidth, height());
}
applyState();
}
static const char* NetworkTab_rowGlyph(const AccessPointInfo& ap) {
if (ap.strength >= 67) {
return "wifi-2";
static const char* NetworkTab_rowGlyph(const AccessPointInfo& ap) {
if (ap.strength >= 67) {
return "wifi-2";
}
if (ap.strength >= 34) {
return "wifi-1";
}
return "wifi-0";
}
if (ap.strength >= 34) {
return "wifi-1";
}
return "wifi-0";
}
private:
void applyState() {
const bool hov = m_inputArea != nullptr && m_inputArea->hovered();
const bool pressed = m_inputArea != nullptr && m_inputArea->pressed();
if (pressed) {
setBackground(roleColor(ColorRole::Primary));
setBorderColor(roleColor(ColorRole::Primary));
setBorderWidth(Style::borderWidth);
private:
void applyState() {
const bool hov = m_inputArea != nullptr && m_inputArea->hovered();
const bool pressed = m_inputArea != nullptr && m_inputArea->pressed();
if (pressed) {
setBackground(roleColor(ColorRole::Primary));
setBorderColor(roleColor(ColorRole::Primary));
setBorderWidth(Style::borderWidth);
if (m_title != nullptr) {
m_title->setColor(roleColor(ColorRole::OnPrimary));
}
return;
}
setBackground(roleColor(m_ap.active ? ColorRole::Secondary : ColorRole::Surface, m_ap.active ? 0.25f : 1.0f));
setBorderColor(roleColor(hov ? ColorRole::Primary : ColorRole::Surface));
setBorderWidth(hov ? Style::borderWidth : 0.0f);
if (m_title != nullptr) {
m_title->setColor(roleColor(ColorRole::OnPrimary));
m_title->setColor(roleColor(ColorRole::OnSurface));
}
return;
}
setBackground(roleColor(m_ap.active ? ColorRole::Secondary : ColorRole::Surface, m_ap.active ? 0.25f : 1.0f));
setBorderColor(roleColor(hov ? ColorRole::Primary : ColorRole::Surface));
setBorderWidth(hov ? Style::borderWidth : 0.0f);
if (m_title != nullptr) {
m_title->setColor(roleColor(ColorRole::OnSurface));
}
}
AccessPointInfo m_ap;
std::function<void(const AccessPointInfo&)> m_onActivate;
std::function<void(const AccessPointInfo&)> m_onForget;
Label* m_title = nullptr;
InputArea* m_inputArea = nullptr;
Button* m_forgetButton = nullptr;
Signal<>::ScopedConnection m_paletteConn;
};
AccessPointInfo m_ap;
std::function<void(const AccessPointInfo&)> m_onActivate;
std::function<void(const AccessPointInfo&)> m_onForget;
Label* m_title = nullptr;
InputArea* m_inputArea = nullptr;
Button* m_forgetButton = nullptr;
Signal<>::ScopedConnection m_paletteConn;
};
} // namespace
+46 -47
View File
@@ -1,6 +1,6 @@
#include "core/ui_phase.h"
#include "shell/control_center/notifications_tab.h"
#include "core/ui_phase.h"
#include "notification/notification_manager.h"
#include "render/core/renderer.h"
#include "shell/panel/panel_manager.h"
@@ -18,58 +18,58 @@ using namespace control_center;
namespace {
constexpr float kNotificationListRightPadding = Style::spaceSm;
constexpr float kNotificationActionButtonSize = Style::controlHeightSm;
constexpr int kSummaryMaxLines = 2;
constexpr int kBodyMaxLines = 3;
constexpr int kExpandedMaxLines = 500;
constexpr float kNotificationListRightPadding = Style::spaceSm;
constexpr float kNotificationActionButtonSize = Style::controlHeightSm;
constexpr int kSummaryMaxLines = 2;
constexpr int kBodyMaxLines = 3;
constexpr int kExpandedMaxLines = 500;
std::string statusText(const NotificationHistoryEntry& entry) {
if (entry.active) {
return "Active";
}
if (!entry.closeReason.has_value()) {
std::string statusText(const NotificationHistoryEntry& entry) {
if (entry.active) {
return "Active";
}
if (!entry.closeReason.has_value()) {
return "Closed";
}
switch (*entry.closeReason) {
case CloseReason::Expired:
return "Expired";
case CloseReason::Dismissed:
return "Dismissed";
case CloseReason::ClosedByCall:
return "Closed";
}
return "Closed";
}
switch (*entry.closeReason) {
case CloseReason::Expired: return "Expired";
case CloseReason::Dismissed: return "Dismissed";
case CloseReason::ClosedByCall: return "Closed";
}
return "Closed";
}
ColorRole statusColorRole(const NotificationHistoryEntry& entry) {
if (entry.active) {
return ColorRole::Primary;
}
if (entry.closeReason == CloseReason::Dismissed) {
return ColorRole::Secondary;
}
return ColorRole::OnSurfaceVariant;
}
void applyNotificationCardStyle(Flex& card, float scale) {
applyOutlinedCard(card, scale);
}
bool canExpandText(Renderer& renderer, std::string_view text, float fontSize, bool bold, float maxWidth,
int collapsedMaxLines) {
if (text.empty()) {
return false;
ColorRole statusColorRole(const NotificationHistoryEntry& entry) {
if (entry.active) {
return ColorRole::Primary;
}
if (entry.closeReason == CloseReason::Dismissed) {
return ColorRole::Secondary;
}
return ColorRole::OnSurfaceVariant;
}
const auto collapsed = renderer.measureText(text, fontSize, bold, maxWidth, collapsedMaxLines);
const auto expanded = renderer.measureText(text, fontSize, bold, maxWidth, kExpandedMaxLines);
const float collapsedHeight = collapsed.bottom - collapsed.top;
const float expandedHeight = expanded.bottom - expanded.top;
return expandedHeight > collapsedHeight + 0.5f;
}
void applyNotificationCardStyle(Flex& card, float scale) { applyOutlinedCard(card, scale); }
bool canExpandText(Renderer& renderer, std::string_view text, float fontSize, bool bold, float maxWidth,
int collapsedMaxLines) {
if (text.empty()) {
return false;
}
const auto collapsed = renderer.measureText(text, fontSize, bold, maxWidth, collapsedMaxLines);
const auto expanded = renderer.measureText(text, fontSize, bold, maxWidth, kExpandedMaxLines);
const float collapsedHeight = collapsed.bottom - collapsed.top;
const float expandedHeight = expanded.bottom - expanded.top;
return expandedHeight > collapsedHeight + 0.5f;
}
} // namespace
NotificationsTab::NotificationsTab(NotificationManager* notifications)
: m_notifications(notifications) {}
NotificationsTab::NotificationsTab(NotificationManager* notifications) : m_notifications(notifications) {}
std::unique_ptr<Flex> NotificationsTab::create() {
const float scale = contentScale();
@@ -318,9 +318,8 @@ void NotificationsTab::rebuild(Renderer& renderer, float width) {
dismiss->setMinHeight(actionButtonSize);
dismiss->setPadding(Style::spaceXs * scale);
dismiss->setRadius(Style::radiusMd * scale);
dismiss->setOnClick([this, id = it->notification.id, active = it->active]() {
removeNotificationEntry(id, active);
});
dismiss->setOnClick(
[this, id = it->notification.id, active = it->active]() { removeNotificationEntry(id, active); });
headerActions->addChild(std::move(dismiss));
header->addChild(std::move(headerActions));
card->addChild(std::move(header));
+4 -4
View File
@@ -24,10 +24,10 @@ using namespace control_center;
namespace {
void styleOverviewCard(Flex& card, float scale) {
applyOutlinedCard(card, scale);
card.setGap(Style::spaceSm * scale);
}
void styleOverviewCard(Flex& card, float scale) {
applyOutlinedCard(card, scale);
card.setGap(Style::spaceSm * scale);
}
} // namespace
+27 -27
View File
@@ -7,35 +7,35 @@
namespace control_center {
void applyOutlinedCard(Flex& card, float scale) {
card.setDirection(FlexDirection::Vertical);
card.setAlign(FlexAlign::Stretch);
card.setGap(Style::spaceSm * scale);
card.setPadding((Style::spaceSm + Style::spaceXs) * scale, Style::spaceMd * scale);
card.setRadius(Style::radiusXl * scale);
card.setBackground(roleColor(ColorRole::Surface, 0.75f));
card.setBorderWidth(Style::borderWidth);
card.setBorderColor(roleColor(ColorRole::Outline));
}
void applyOutlinedCard(Flex& card, float scale) {
card.setDirection(FlexDirection::Vertical);
card.setAlign(FlexAlign::Stretch);
card.setGap(Style::spaceSm * scale);
card.setPadding((Style::spaceSm + Style::spaceXs) * scale, Style::spaceMd * scale);
card.setRadius(Style::radiusXl * scale);
card.setBackground(roleColor(ColorRole::Surface, 0.75f));
card.setBorderWidth(Style::borderWidth);
card.setBorderColor(roleColor(ColorRole::Outline));
}
Label* addTitle(Flex& parent, const std::string& text, float scale) {
auto label = std::make_unique<Label>();
label->setText(text);
label->setBold(true);
label->setFontSize(Style::fontSizeTitle * scale);
label->setColor(roleColor(ColorRole::OnSurface));
auto* ptr = label.get();
parent.addChild(std::move(label));
return ptr;
}
Label* addTitle(Flex& parent, const std::string& text, float scale) {
auto label = std::make_unique<Label>();
label->setText(text);
label->setBold(true);
label->setFontSize(Style::fontSizeTitle * scale);
label->setColor(roleColor(ColorRole::OnSurface));
auto* ptr = label.get();
parent.addChild(std::move(label));
return ptr;
}
void addBody(Flex& parent, const std::string& text, float scale) {
auto label = std::make_unique<Label>();
label->setText(text);
label->setFontSize(Style::fontSizeBody * scale);
label->setColor(roleColor(ColorRole::OnSurfaceVariant));
parent.addChild(std::move(label));
}
void addBody(Flex& parent, const std::string& text, float scale) {
auto label = std::make_unique<Label>();
label->setText(text);
label->setFontSize(Style::fontSizeBody * scale);
label->setColor(roleColor(ColorRole::OnSurfaceVariant));
parent.addChild(std::move(label));
}
} // namespace control_center
+14 -13
View File
@@ -285,8 +285,7 @@ void WeatherTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeig
if (m_currentCard != nullptr && m_currentGlyph != nullptr) {
const float cardInnerWidth =
std::max(0.0f, m_currentCard->width() - (m_currentCard->paddingLeft() + m_currentCard->paddingRight()));
currentTextWidth =
std::max(1.0f, cardInnerWidth - m_currentGlyph->width() - m_currentCard->gap());
currentTextWidth = std::max(1.0f, cardInnerWidth - m_currentGlyph->width() - m_currentCard->gap());
}
if (m_currentTempLabel != nullptr) {
m_currentTempLabel->setMaxWidth(currentTextWidth);
@@ -313,7 +312,8 @@ void WeatherTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeig
if (m_statusLabel != nullptr) {
m_statusLabel->setMaxWidth(leftColumnWidth);
}
for (auto* label : {m_windLabel, m_sunriseLabel, m_sunsetLabel, m_timezoneLabel, m_longitudeLabel, m_elevationLabel}) {
for (auto* label :
{m_windLabel, m_sunriseLabel, m_sunsetLabel, m_timezoneLabel, m_longitudeLabel, m_elevationLabel}) {
if (label != nullptr) {
label->setMaxWidth(leftColumnWidth);
}
@@ -339,7 +339,8 @@ void WeatherTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeig
if (m_currentGlyph != nullptr && m_currentText != nullptr && m_currentCard != nullptr) {
const float cardInnerHeight =
std::max(0.0f, m_currentCard->height() - (m_currentCard->paddingTop() + m_currentCard->paddingBottom()));
const float desiredGlyph = std::max(Style::controlHeightLg * 1.8f * scale, std::min(m_currentText->height(), cardInnerHeight));
const float desiredGlyph =
std::max(Style::controlHeightLg * 1.8f * scale, std::min(m_currentText->height(), cardInnerHeight));
m_currentGlyph->setGlyphSize(desiredGlyph);
}
@@ -351,8 +352,8 @@ void WeatherTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeig
}
}
if (visibleRows > 0) {
const float detailsInnerHeight = std::max(
0.0f, detailsTargetHeight - (m_detailsCard->paddingTop() + m_detailsCard->paddingBottom()));
const float detailsInnerHeight =
std::max(0.0f, detailsTargetHeight - (m_detailsCard->paddingTop() + m_detailsCard->paddingBottom()));
const float gapsTotal = m_detailsCard->gap() * static_cast<float>(visibleRows - 1);
const float rowHeight =
std::max(Style::controlHeightSm * scale, (detailsInnerHeight - gapsTotal) / static_cast<float>(visibleRows));
@@ -376,8 +377,8 @@ void WeatherTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeig
const float forecastInnerHeight = std::max(
0.0f, m_forecastColumn->height() - (m_forecastColumn->paddingTop() + m_forecastColumn->paddingBottom()));
const float gapsTotal = m_forecastColumn->gap() * static_cast<float>(visibleForecastDays - 1);
const float rowHeight = std::max(Style::controlHeightLg * scale, (forecastInnerHeight - gapsTotal) /
static_cast<float>(visibleForecastDays));
const float rowHeight = std::max(Style::controlHeightLg * scale,
(forecastInnerHeight - gapsTotal) / static_cast<float>(visibleForecastDays));
for (std::size_t i = 0; i < kDayCount; ++i) {
if (m_dayCards[i] == nullptr) {
@@ -566,11 +567,11 @@ void WeatherTab::sync(Renderer& renderer) {
m_weather->displayTemperatureUnit()));
if (m_currentHiLoLabel != nullptr) {
if (!snapshot.forecastDays.empty()) {
m_currentHiLoLabel->setText(
std::format("H/L {} / {}{}", static_cast<int>(std::lround(m_weather->displayTemperature(
snapshot.forecastDays.front().temperatureMaxC))),
static_cast<int>(std::lround(m_weather->displayTemperature(snapshot.forecastDays.front().temperatureMinC))),
m_weather->displayTemperatureUnit()));
m_currentHiLoLabel->setText(std::format(
"H/L {} / {}{}",
static_cast<int>(std::lround(m_weather->displayTemperature(snapshot.forecastDays.front().temperatureMaxC))),
static_cast<int>(std::lround(m_weather->displayTemperature(snapshot.forecastDays.front().temperatureMinC))),
m_weather->displayTemperatureUnit()));
} else {
m_currentHiLoLabel->setText("H/L -- / --");
}
+36 -35
View File
@@ -7,49 +7,49 @@
namespace {
constexpr Logger kLog("desktop");
constexpr Logger kLog("desktop");
std::string getStringSetting(const std::unordered_map<std::string, WidgetSettingValue>& settings, const std::string& key,
const std::string& fallback = {}) {
const auto it = settings.find(key);
if (it == settings.end()) {
std::string getStringSetting(const std::unordered_map<std::string, WidgetSettingValue>& settings,
const std::string& key, const std::string& fallback = {}) {
const auto it = settings.find(key);
if (it == settings.end()) {
return fallback;
}
if (const auto* value = std::get_if<std::string>(&it->second)) {
return *value;
}
return fallback;
}
if (const auto* value = std::get_if<std::string>(&it->second)) {
return *value;
}
return fallback;
}
float getFloatSetting(const std::unordered_map<std::string, WidgetSettingValue>& settings, const std::string& key,
float fallback) {
const auto it = settings.find(key);
if (it == settings.end()) {
float getFloatSetting(const std::unordered_map<std::string, WidgetSettingValue>& settings, const std::string& key,
float fallback) {
const auto it = settings.find(key);
if (it == settings.end()) {
return fallback;
}
if (const auto* value = std::get_if<double>(&it->second)) {
return static_cast<float>(*value);
}
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
return static_cast<float>(*value);
}
return fallback;
}
if (const auto* value = std::get_if<double>(&it->second)) {
return static_cast<float>(*value);
}
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
return static_cast<float>(*value);
}
return fallback;
}
int getIntSetting(const std::unordered_map<std::string, WidgetSettingValue>& settings, const std::string& key,
int fallback) {
const auto it = settings.find(key);
if (it == settings.end()) {
int getIntSetting(const std::unordered_map<std::string, WidgetSettingValue>& settings, const std::string& key,
int fallback) {
const auto it = settings.find(key);
if (it == settings.end()) {
return fallback;
}
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
return static_cast<int>(*value);
}
if (const auto* value = std::get_if<double>(&it->second)) {
return static_cast<int>(*value);
}
return fallback;
}
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
return static_cast<int>(*value);
}
if (const auto* value = std::get_if<double>(&it->second)) {
return static_cast<int>(*value);
}
return fallback;
}
} // namespace
@@ -65,7 +65,8 @@ DesktopWidgetFactory::create(const std::string& type,
kLog.warn("desktop widget factory: clock requires TimeService");
return nullptr;
}
auto widget = std::make_unique<DesktopClockWidget>(*m_timeService, getStringSetting(settings, "format", "{:%H:%M}"));
auto widget =
std::make_unique<DesktopClockWidget>(*m_timeService, getStringSetting(settings, "format", "{:%H:%M}"));
widget->setContentScale(contentScale);
return widget;
}
+3 -3
View File
@@ -14,9 +14,9 @@ class DesktopWidgetFactory {
public:
DesktopWidgetFactory(TimeService* timeService, PipeWireSpectrum* pipewireSpectrum);
[[nodiscard]] std::unique_ptr<DesktopWidget> create(const std::string& type,
const std::unordered_map<std::string, WidgetSettingValue>& settings,
float contentScale = 1.0f) const;
[[nodiscard]] std::unique_ptr<DesktopWidget>
create(const std::string& type, const std::unordered_map<std::string, WidgetSettingValue>& settings,
float contentScale = 1.0f) const;
private:
TimeService* m_timeService = nullptr;
+143 -144
View File
@@ -9,8 +9,8 @@
#include "shell/desktop/widget_transform.h"
#include "wayland/wayland_connection.h"
#include <cstdlib>
#include <charconv>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <limits>
@@ -18,170 +18,170 @@
namespace {
constexpr Logger kLog("desktop");
constexpr std::string_view kDesktopWidgetIdPrefix = "desktop-widget-";
constexpr Logger kLog("desktop");
constexpr std::string_view kDesktopWidgetIdPrefix = "desktop-widget-";
std::string stateDir() {
const char* xdg = std::getenv("XDG_STATE_HOME");
if (xdg != nullptr && xdg[0] != '\0') {
return std::string(xdg) + "/noctalia";
}
const char* home = std::getenv("HOME");
if (home != nullptr && home[0] != '\0') {
return std::string(home) + "/.local/state/noctalia";
}
return {};
}
std::string outputKey(const WaylandOutput& output) {
if (!output.connectorName.empty()) {
return output.connectorName;
}
return std::to_string(output.name);
}
const WaylandOutput* resolveEffectiveOutput(const WaylandConnection& wayland, const std::string& requestedOutput) {
const auto& outputs = wayland.outputs();
const WaylandOutput* primary = nullptr;
for (const auto& output : outputs) {
if (!output.done || output.output == nullptr) {
continue;
std::string stateDir() {
const char* xdg = std::getenv("XDG_STATE_HOME");
if (xdg != nullptr && xdg[0] != '\0') {
return std::string(xdg) + "/noctalia";
}
if (primary == nullptr) {
primary = &output;
const char* home = std::getenv("HOME");
if (home != nullptr && home[0] != '\0') {
return std::string(home) + "/.local/state/noctalia";
}
if (!requestedOutput.empty() && outputKey(output) == requestedOutput) {
return &output;
return {};
}
std::string outputKey(const WaylandOutput& output) {
if (!output.connectorName.empty()) {
return output.connectorName;
}
return std::to_string(output.name);
}
return primary;
}
float outputLogicalWidth(const WaylandOutput& output) {
if (output.logicalWidth > 0) {
return static_cast<float>(output.logicalWidth);
}
return static_cast<float>(std::max(1, output.width / std::max(1, output.scale)));
}
float outputLogicalHeight(const WaylandOutput& output) {
if (output.logicalHeight > 0) {
return static_cast<float>(output.logicalHeight);
}
return static_cast<float>(std::max(1, output.height / std::max(1, output.scale)));
}
bool formatShowsSeconds(const DesktopWidgetState& state) {
if (state.type != "clock") {
return false;
}
const auto it = state.settings.find("format");
if (it == state.settings.end()) {
return false;
}
const auto* format = std::get_if<std::string>(&it->second);
if (format == nullptr) {
return false;
}
return format->find("%S") != std::string::npos || format->find("%T") != std::string::npos ||
format->find("%X") != std::string::npos;
}
float estimateIntrinsicWidth(const DesktopWidgetState& state) {
if (state.type == "clock") {
return formatShowsSeconds(state) ? 270.0f : 210.0f;
}
if (state.type == "audio_visualizer") {
const auto it = state.settings.find("width");
if (it != state.settings.end()) {
if (const auto* value = std::get_if<double>(&it->second)) {
return static_cast<float>(*value);
const WaylandOutput* resolveEffectiveOutput(const WaylandConnection& wayland, const std::string& requestedOutput) {
const auto& outputs = wayland.outputs();
const WaylandOutput* primary = nullptr;
for (const auto& output : outputs) {
if (!output.done || output.output == nullptr) {
continue;
}
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
return static_cast<float>(*value);
if (primary == nullptr) {
primary = &output;
}
if (!requestedOutput.empty() && outputKey(output) == requestedOutput) {
return &output;
}
}
return 240.0f;
return primary;
}
return 210.0f;
}
float estimateIntrinsicHeight(const DesktopWidgetState& state) {
if (state.type == "audio_visualizer") {
const auto it = state.settings.find("height");
if (it != state.settings.end()) {
if (const auto* value = std::get_if<double>(&it->second)) {
return static_cast<float>(*value);
}
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
return static_cast<float>(*value);
}
float outputLogicalWidth(const WaylandOutput& output) {
if (output.logicalWidth > 0) {
return static_cast<float>(output.logicalWidth);
}
return 96.0f;
return static_cast<float>(std::max(1, output.width / std::max(1, output.scale)));
}
return 70.0f;
}
void writeSetting(toml::table& table, const std::string& key, const WidgetSettingValue& value) {
std::visit(
[&](const auto& concrete) {
using T = std::decay_t<decltype(concrete)>;
if constexpr (std::is_same_v<T, std::vector<std::string>>) {
toml::array array;
for (const auto& item : concrete) {
array.push_back(item);
}
table.insert_or_assign(key, std::move(array));
} else {
table.insert_or_assign(key, concrete);
float outputLogicalHeight(const WaylandOutput& output) {
if (output.logicalHeight > 0) {
return static_cast<float>(output.logicalHeight);
}
return static_cast<float>(std::max(1, output.height / std::max(1, output.scale)));
}
bool formatShowsSeconds(const DesktopWidgetState& state) {
if (state.type != "clock") {
return false;
}
const auto it = state.settings.find("format");
if (it == state.settings.end()) {
return false;
}
const auto* format = std::get_if<std::string>(&it->second);
if (format == nullptr) {
return false;
}
return format->find("%S") != std::string::npos || format->find("%T") != std::string::npos ||
format->find("%X") != std::string::npos;
}
float estimateIntrinsicWidth(const DesktopWidgetState& state) {
if (state.type == "clock") {
return formatShowsSeconds(state) ? 270.0f : 210.0f;
}
if (state.type == "audio_visualizer") {
const auto it = state.settings.find("width");
if (it != state.settings.end()) {
if (const auto* value = std::get_if<double>(&it->second)) {
return static_cast<float>(*value);
}
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
return static_cast<float>(*value);
}
},
value);
}
std::optional<WidgetSettingValue> readSetting(const toml::node& node) {
if (const auto* stringValue = node.as_string()) {
return WidgetSettingValue{stringValue->get()};
}
if (const auto* intValue = node.as_integer()) {
return WidgetSettingValue{intValue->get()};
}
if (const auto* floatValue = node.as_floating_point()) {
return WidgetSettingValue{floatValue->get()};
}
if (const auto* boolValue = node.as_boolean()) {
return WidgetSettingValue{boolValue->get()};
}
if (const auto* arrayValue = node.as_array()) {
std::vector<std::string> strings;
for (const auto& item : *arrayValue) {
if (auto value = item.value<std::string>()) {
strings.push_back(*value);
}
return 240.0f;
}
return WidgetSettingValue{std::move(strings)};
}
return std::nullopt;
}
bool parseDesktopWidgetCounter(std::string_view id, std::uint64_t& value) {
if (!id.starts_with(kDesktopWidgetIdPrefix)) {
return false;
return 210.0f;
}
const std::string_view suffix = id.substr(kDesktopWidgetIdPrefix.size());
if (suffix.empty()) {
return false;
float estimateIntrinsicHeight(const DesktopWidgetState& state) {
if (state.type == "audio_visualizer") {
const auto it = state.settings.find("height");
if (it != state.settings.end()) {
if (const auto* value = std::get_if<double>(&it->second)) {
return static_cast<float>(*value);
}
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
return static_cast<float>(*value);
}
}
return 96.0f;
}
return 70.0f;
}
value = 0;
const auto* begin = suffix.data();
const auto* end = suffix.data() + suffix.size();
const auto [ptr, ec] = std::from_chars(begin, end, value, 16);
return ec == std::errc{} && ptr == end;
}
void writeSetting(toml::table& table, const std::string& key, const WidgetSettingValue& value) {
std::visit(
[&](const auto& concrete) {
using T = std::decay_t<decltype(concrete)>;
if constexpr (std::is_same_v<T, std::vector<std::string>>) {
toml::array array;
for (const auto& item : concrete) {
array.push_back(item);
}
table.insert_or_assign(key, std::move(array));
} else {
table.insert_or_assign(key, concrete);
}
},
value);
}
std::string makeDesktopWidgetId(std::uint64_t counter) { return std::format("desktop-widget-{:016x}", counter); }
std::optional<WidgetSettingValue> readSetting(const toml::node& node) {
if (const auto* stringValue = node.as_string()) {
return WidgetSettingValue{stringValue->get()};
}
if (const auto* intValue = node.as_integer()) {
return WidgetSettingValue{intValue->get()};
}
if (const auto* floatValue = node.as_floating_point()) {
return WidgetSettingValue{floatValue->get()};
}
if (const auto* boolValue = node.as_boolean()) {
return WidgetSettingValue{boolValue->get()};
}
if (const auto* arrayValue = node.as_array()) {
std::vector<std::string> strings;
for (const auto& item : *arrayValue) {
if (auto value = item.value<std::string>()) {
strings.push_back(*value);
}
}
return WidgetSettingValue{std::move(strings)};
}
return std::nullopt;
}
bool parseDesktopWidgetCounter(std::string_view id, std::uint64_t& value) {
if (!id.starts_with(kDesktopWidgetIdPrefix)) {
return false;
}
const std::string_view suffix = id.substr(kDesktopWidgetIdPrefix.size());
if (suffix.empty()) {
return false;
}
value = 0;
const auto* begin = suffix.data();
const auto* end = suffix.data() + suffix.size();
const auto [ptr, ec] = std::from_chars(begin, end, value, 16);
return ec == std::errc{} && ptr == end;
}
std::string makeDesktopWidgetId(std::uint64_t counter) { return std::format("desktop-widget-{:016x}", counter); }
} // namespace
@@ -207,7 +207,6 @@ void DesktopWidgetsController::initialize(WaylandConnection& wayland, ConfigServ
if (m_config != nullptr) {
m_config->addReloadCallback([this]() { applyVisibility(); });
}
}
void DesktopWidgetsController::registerIpc(IpcService& ipc) {
+122 -122
View File
@@ -33,118 +33,118 @@
namespace {
constexpr Logger kLog("desktop");
constexpr float kToolbarY = 68.0f;
constexpr float kSelectionStroke = 2.0f;
constexpr float kRotatePadding = 14.0f;
constexpr float kHandleSize = 14.0f;
constexpr float kMinScale = 0.2f;
constexpr float kMaxScale = 8.0f;
constexpr float kRotationSnap = static_cast<float>(M_PI) / 12.0f;
constexpr std::array<const char*, 2> kWidgetTypeLabels{"Clock", "Audio Visualizer"};
constexpr std::string_view kDesktopWidgetIdPrefix = "desktop-widget-";
constexpr Logger kLog("desktop");
constexpr float kToolbarY = 68.0f;
constexpr float kSelectionStroke = 2.0f;
constexpr float kRotatePadding = 14.0f;
constexpr float kHandleSize = 14.0f;
constexpr float kMinScale = 0.2f;
constexpr float kMaxScale = 8.0f;
constexpr float kRotationSnap = static_cast<float>(M_PI) / 12.0f;
constexpr std::array<const char*, 2> kWidgetTypeLabels{"Clock", "Audio Visualizer"};
constexpr std::string_view kDesktopWidgetIdPrefix = "desktop-widget-";
std::string outputKey(const WaylandOutput& output) {
if (!output.connectorName.empty()) {
return output.connectorName;
}
return std::to_string(output.name);
}
const WaylandOutput* resolveEffectiveOutput(const WaylandConnection& wayland, const std::string& requestedOutput) {
const auto& outputs = wayland.outputs();
const WaylandOutput* primary = nullptr;
for (const auto& output : outputs) {
if (!output.done || output.output == nullptr) {
continue;
std::string outputKey(const WaylandOutput& output) {
if (!output.connectorName.empty()) {
return output.connectorName;
}
if (primary == nullptr) {
primary = &output;
return std::to_string(output.name);
}
const WaylandOutput* resolveEffectiveOutput(const WaylandConnection& wayland, const std::string& requestedOutput) {
const auto& outputs = wayland.outputs();
const WaylandOutput* primary = nullptr;
for (const auto& output : outputs) {
if (!output.done || output.output == nullptr) {
continue;
}
if (primary == nullptr) {
primary = &output;
}
if (!requestedOutput.empty() && outputKey(output) == requestedOutput) {
return &output;
}
}
if (!requestedOutput.empty() && outputKey(output) == requestedOutput) {
return &output;
return primary;
}
float snapToGrid(float value, std::int32_t cellSize) {
if (cellSize <= 0) {
return value;
}
}
return primary;
}
float snapToGrid(float value, std::int32_t cellSize) {
if (cellSize <= 0) {
return value;
}
return std::round(value / static_cast<float>(cellSize)) * static_cast<float>(cellSize);
}
float normalizeAngle(float radians) {
while (radians > static_cast<float>(M_PI)) {
radians -= static_cast<float>(M_PI * 2.0);
}
while (radians < -static_cast<float>(M_PI)) {
radians += static_cast<float>(M_PI * 2.0);
}
return radians;
}
float outputLogicalWidth(const WaylandOutput& output) {
if (output.logicalWidth > 0) {
return static_cast<float>(output.logicalWidth);
}
return static_cast<float>(std::max(1, output.width / std::max(1, output.scale)));
}
float outputLogicalHeight(const WaylandOutput& output) {
if (output.logicalHeight > 0) {
return static_cast<float>(output.logicalHeight);
}
return static_cast<float>(std::max(1, output.height / std::max(1, output.scale)));
}
bool formatShowsSeconds(const DesktopWidgetState& state) {
if (state.type != "clock") {
return false;
}
const auto it = state.settings.find("format");
if (it == state.settings.end()) {
return false;
}
const auto* format = std::get_if<std::string>(&it->second);
if (format == nullptr) {
return false;
}
return format->find("%S") != std::string::npos || format->find("%T") != std::string::npos ||
format->find("%X") != std::string::npos;
}
bool parseDesktopWidgetCounter(std::string_view id, std::uint64_t& value) {
if (!id.starts_with(kDesktopWidgetIdPrefix)) {
return false;
return std::round(value / static_cast<float>(cellSize)) * static_cast<float>(cellSize);
}
const std::string_view suffix = id.substr(kDesktopWidgetIdPrefix.size());
if (suffix.empty()) {
return false;
}
value = 0;
const auto* begin = suffix.data();
const auto* end = suffix.data() + suffix.size();
const auto [ptr, ec] = std::from_chars(begin, end, value, 16);
return ec == std::errc{} && ptr == end;
}
std::string nextDesktopWidgetId(const DesktopWidgetsSnapshot& snapshot) {
std::uint64_t maxCounter = 0;
for (const auto& widget : snapshot.widgets) {
std::uint64_t counter = 0;
if (parseDesktopWidgetCounter(widget.id, counter)) {
maxCounter = std::max(maxCounter, counter);
float normalizeAngle(float radians) {
while (radians > static_cast<float>(M_PI)) {
radians -= static_cast<float>(M_PI * 2.0);
}
while (radians < -static_cast<float>(M_PI)) {
radians += static_cast<float>(M_PI * 2.0);
}
return radians;
}
const std::uint64_t nextCounter =
maxCounter == std::numeric_limits<std::uint64_t>::max() ? maxCounter : (maxCounter + 1);
return std::format("desktop-widget-{:016x}", nextCounter);
}
float outputLogicalWidth(const WaylandOutput& output) {
if (output.logicalWidth > 0) {
return static_cast<float>(output.logicalWidth);
}
return static_cast<float>(std::max(1, output.width / std::max(1, output.scale)));
}
float outputLogicalHeight(const WaylandOutput& output) {
if (output.logicalHeight > 0) {
return static_cast<float>(output.logicalHeight);
}
return static_cast<float>(std::max(1, output.height / std::max(1, output.scale)));
}
bool formatShowsSeconds(const DesktopWidgetState& state) {
if (state.type != "clock") {
return false;
}
const auto it = state.settings.find("format");
if (it == state.settings.end()) {
return false;
}
const auto* format = std::get_if<std::string>(&it->second);
if (format == nullptr) {
return false;
}
return format->find("%S") != std::string::npos || format->find("%T") != std::string::npos ||
format->find("%X") != std::string::npos;
}
bool parseDesktopWidgetCounter(std::string_view id, std::uint64_t& value) {
if (!id.starts_with(kDesktopWidgetIdPrefix)) {
return false;
}
const std::string_view suffix = id.substr(kDesktopWidgetIdPrefix.size());
if (suffix.empty()) {
return false;
}
value = 0;
const auto* begin = suffix.data();
const auto* end = suffix.data() + suffix.size();
const auto [ptr, ec] = std::from_chars(begin, end, value, 16);
return ec == std::errc{} && ptr == end;
}
std::string nextDesktopWidgetId(const DesktopWidgetsSnapshot& snapshot) {
std::uint64_t maxCounter = 0;
for (const auto& widget : snapshot.widgets) {
std::uint64_t counter = 0;
if (parseDesktopWidgetCounter(widget.id, counter)) {
maxCounter = std::max(maxCounter, counter);
}
}
const std::uint64_t nextCounter =
maxCounter == std::numeric_limits<std::uint64_t>::max() ? maxCounter : (maxCounter + 1);
return std::format("desktop-widget-{:016x}", nextCounter);
}
} // namespace
@@ -410,8 +410,8 @@ void DesktopWidgetsEditor::rebuildScene(OverlaySurface& surface) {
view.transformNode->setRotation(widgetState.rotationRad);
view.transformNode->setScale(widgetState.scale);
view.transformNode->setZIndex(4);
view.bodyArea->setOnPress([this, id = widgetState.id, w = view.intrinsicWidth, h = view.intrinsicHeight](
const InputArea::PointerData& data) {
view.bodyArea->setOnPress([this, id = widgetState.id, w = view.intrinsicWidth,
h = view.intrinsicHeight](const InputArea::PointerData& data) {
if (data.button != BTN_LEFT) {
return;
}
@@ -595,12 +595,13 @@ void DesktopWidgetsEditor::updateSelectionVisuals(OverlaySurface& surface) {
return;
}
const WidgetTransformBounds bounds = computeWidgetTransformBounds(
state->cx, state->cy, selectedIt->second.intrinsicWidth, selectedIt->second.intrinsicHeight, state->scale,
state->rotationRad);
const WidgetTransformBounds bounds =
computeWidgetTransformBounds(state->cx, state->cy, selectedIt->second.intrinsicWidth,
selectedIt->second.intrinsicHeight, state->scale, state->rotationRad);
surface.rotationRing->setPosition(bounds.left - kRotatePadding, bounds.top - kRotatePadding);
surface.rotationRing->setFrameSize(bounds.aabbWidth + kRotatePadding * 2.0f, bounds.aabbHeight + kRotatePadding * 2.0f);
surface.rotationRing->setFrameSize(bounds.aabbWidth + kRotatePadding * 2.0f,
bounds.aabbHeight + kRotatePadding * 2.0f);
surface.rotateArea->setPosition(bounds.left - kRotatePadding, bounds.top - kRotatePadding);
surface.rotateArea->setFrameSize(bounds.aabbWidth + kRotatePadding * 2.0f, bounds.aabbHeight + kRotatePadding * 2.0f);
@@ -641,10 +642,10 @@ void DesktopWidgetsEditor::addWidget(const std::string& outputName, const std::s
float centerX = 320.0f;
float centerY = 240.0f;
if (const WaylandOutput* output = resolveEffectiveOutput(*m_wayland, outputName); output != nullptr) {
const int logicalWidth = output->logicalWidth > 0 ? output->logicalWidth
: output->width / std::max(1, output->scale);
const int logicalHeight = output->logicalHeight > 0 ? output->logicalHeight
: output->height / std::max(1, output->scale);
const int logicalWidth =
output->logicalWidth > 0 ? output->logicalWidth : output->width / std::max(1, output->scale);
const int logicalHeight =
output->logicalHeight > 0 ? output->logicalHeight : output->height / std::max(1, output->scale);
centerX = static_cast<float>(std::max(1, logicalWidth)) * 0.5f;
centerY = static_cast<float>(std::max(1, logicalHeight)) * 0.5f;
}
@@ -719,8 +720,8 @@ void DesktopWidgetsEditor::updateDrag() {
state->cy = snapToGrid(state->cy, m_snapshot.grid.cellSize);
}
} else if (m_drag.mode == DragMode::Rotate) {
const float startAngle = std::atan2(m_drag.startSceneY - m_drag.initialState.cy,
m_drag.startSceneX - m_drag.initialState.cx);
const float startAngle =
std::atan2(m_drag.startSceneY - m_drag.initialState.cy, m_drag.startSceneX - m_drag.initialState.cx);
const float currentAngle =
std::atan2(m_currentEventSceneY - m_drag.initialState.cy, m_currentEventSceneX - m_drag.initialState.cx);
float rotation = normalizeAngle(m_drag.initialState.rotationRad + (currentAngle - startAngle));
@@ -732,9 +733,9 @@ void DesktopWidgetsEditor::updateDrag() {
if (shouldSnap()) {
const float snappedCornerX = snapToGrid(m_currentEventSceneX, m_snapshot.grid.cellSize);
const float snappedCornerY = snapToGrid(m_currentEventSceneY, m_snapshot.grid.cellSize);
const WidgetTransformBounds baseBounds = computeWidgetTransformBounds(
m_drag.initialState.cx, m_drag.initialState.cy, m_drag.intrinsicWidth, m_drag.intrinsicHeight, 1.0f,
m_drag.initialState.rotationRad);
const WidgetTransformBounds baseBounds =
computeWidgetTransformBounds(m_drag.initialState.cx, m_drag.initialState.cy, m_drag.intrinsicWidth,
m_drag.intrinsicHeight, 1.0f, m_drag.initialState.rotationRad);
const float scaleX =
std::max(0.0f, (snappedCornerX - m_drag.initialState.cx) * 2.0f / std::max(1.0f, baseBounds.aabbWidth));
const float scaleY =
@@ -757,10 +758,9 @@ void DesktopWidgetsEditor::updateDrag() {
if (m_wayland != nullptr) {
if (const WaylandOutput* output = resolveEffectiveOutput(*m_wayland, state->outputName); output != nullptr) {
const WidgetTransformClampResult clamped =
clampWidgetCenterToOutput(state->cx, state->cy, m_drag.intrinsicWidth, m_drag.intrinsicHeight, state->scale,
state->rotationRad, outputLogicalWidth(*output), outputLogicalHeight(*output),
kDesktopWidgetMinVisibleFraction);
const WidgetTransformClampResult clamped = clampWidgetCenterToOutput(
state->cx, state->cy, m_drag.intrinsicWidth, m_drag.intrinsicHeight, state->scale, state->rotationRad,
outputLogicalWidth(*output), outputLogicalHeight(*output), kDesktopWidgetMinVisibleFraction);
state->cx = clamped.cx;
state->cy = clamped.cy;
}
+3 -2
View File
@@ -1,8 +1,8 @@
#pragma once
#include "render/scene/node.h"
#include "render/animation/animation_manager.h"
#include "render/scene/input_dispatcher.h"
#include "render/scene/node.h"
#include "shell/desktop/desktop_widget_factory.h"
#include "shell/desktop/desktop_widgets_controller.h"
#include "wayland/layer_surface.h"
@@ -100,7 +100,8 @@ private:
void addWidget(const std::string& outputName, const std::string& type);
void removeSelectedWidget();
void requestExit();
void startDrag(DragMode mode, const std::string& widgetId, float intrinsicWidth, float intrinsicHeight, bool rebuildOnFinish);
void startDrag(DragMode mode, const std::string& widgetId, float intrinsicWidth, float intrinsicHeight,
bool rebuildOnFinish);
void updateDrag();
void finishDrag();
[[nodiscard]] OverlaySurface* findSurface(wl_surface* surface);
+60 -59
View File
@@ -16,54 +16,54 @@
namespace {
constexpr Logger kLog("desktop");
constexpr Logger kLog("desktop");
std::string outputKey(const WaylandOutput& output) {
if (!output.connectorName.empty()) {
return output.connectorName;
}
return std::to_string(output.name);
}
float outputLogicalWidth(const WaylandOutput& output) {
if (output.logicalWidth > 0) {
return static_cast<float>(output.logicalWidth);
}
return static_cast<float>(std::max(1, output.width / std::max(1, output.scale)));
}
float outputLogicalHeight(const WaylandOutput& output) {
if (output.logicalHeight > 0) {
return static_cast<float>(output.logicalHeight);
}
return static_cast<float>(std::max(1, output.height / std::max(1, output.scale)));
}
const WaylandOutput* resolveEffectiveOutput(const WaylandConnection& wayland, const std::string& requestedOutput) {
const auto& outputs = wayland.outputs();
const WaylandOutput* primary = nullptr;
for (const auto& output : outputs) {
if (!output.done || output.output == nullptr) {
continue;
}
if (primary == nullptr) {
primary = &output;
}
if (!requestedOutput.empty() && outputKey(output) == requestedOutput) {
return &output;
std::string outputKey(const WaylandOutput& output) {
if (!output.connectorName.empty()) {
return output.connectorName;
}
return std::to_string(output.name);
}
return primary;
}
DesktopWidgetState* findStateById(DesktopWidgetsSnapshot& snapshot, const std::string& id) {
for (auto& widget : snapshot.widgets) {
if (widget.id == id) {
return &widget;
float outputLogicalWidth(const WaylandOutput& output) {
if (output.logicalWidth > 0) {
return static_cast<float>(output.logicalWidth);
}
return static_cast<float>(std::max(1, output.width / std::max(1, output.scale)));
}
float outputLogicalHeight(const WaylandOutput& output) {
if (output.logicalHeight > 0) {
return static_cast<float>(output.logicalHeight);
}
return static_cast<float>(std::max(1, output.height / std::max(1, output.scale)));
}
const WaylandOutput* resolveEffectiveOutput(const WaylandConnection& wayland, const std::string& requestedOutput) {
const auto& outputs = wayland.outputs();
const WaylandOutput* primary = nullptr;
for (const auto& output : outputs) {
if (!output.done || output.output == nullptr) {
continue;
}
if (primary == nullptr) {
primary = &output;
}
if (!requestedOutput.empty() && outputKey(output) == requestedOutput) {
return &output;
}
}
return primary;
}
DesktopWidgetState* findStateById(DesktopWidgetsSnapshot& snapshot, const std::string& id) {
for (auto& widget : snapshot.widgets) {
if (widget.id == id) {
return &widget;
}
}
return nullptr;
}
return nullptr;
}
} // namespace
@@ -148,9 +148,8 @@ void DesktopWidgetsHost::syncInstances() {
return;
}
std::erase_if(m_instances, [this](const auto& instance) {
return findStateById(m_snapshot, instance->state.id) == nullptr;
});
std::erase_if(m_instances,
[this](const auto& instance) { return findStateById(m_snapshot, instance->state.id) == nullptr; });
for (const auto& state : m_snapshot.widgets) {
const WaylandOutput* output = resolveEffectiveOutput(*m_wayland, state.outputName);
@@ -165,9 +164,9 @@ void DesktopWidgetsHost::syncInstances() {
}
const std::string effectiveOutputName = outputKey(*output);
const bool widgetDefinitionChanged =
existing->state.type != state.type || existing->state.settings != state.settings ||
existing->effectiveOutputName != effectiveOutputName;
const bool widgetDefinitionChanged = existing->state.type != state.type ||
existing->state.settings != state.settings ||
existing->effectiveOutputName != effectiveOutputName;
if (widgetDefinitionChanged) {
std::erase_if(m_instances, [&state](const auto& instance) { return instance->state.id == state.id; });
@@ -189,8 +188,8 @@ void DesktopWidgetsHost::createInstance(const DesktopWidgetState& state, const W
return;
}
auto widget = m_factory->create(state.type, state.settings,
m_config != nullptr ? m_config->config().shell.uiScale : 1.0f);
auto widget =
m_factory->create(state.type, state.settings, m_config != nullptr ? m_config->config().shell.uiScale : 1.0f);
if (widget == nullptr) {
return;
}
@@ -240,8 +239,9 @@ void DesktopWidgetsHost::createInstance(const DesktopWidgetState& state, const W
instance->surface->setConfigureCallback(
[rawInstance](std::uint32_t /*width*/, std::uint32_t /*height*/) { rawInstance->surface->requestLayout(); });
instance->surface->setPrepareFrameCallback(
[this, rawInstance](bool needsUpdate, bool needsLayout) { prepareFrame(*rawInstance, needsUpdate, needsLayout); });
instance->surface->setPrepareFrameCallback([this, rawInstance](bool needsUpdate, bool needsLayout) {
prepareFrame(*rawInstance, needsUpdate, needsLayout);
});
instance->surface->setFrameTickCallback([this, rawInstance](float deltaMs) {
if (rawInstance->widget == nullptr || m_renderContext == nullptr) {
return;
@@ -292,7 +292,8 @@ void DesktopWidgetsHost::prepareFrame(DesktopWidgetInstance& instance, bool need
}
if (m_wayland != nullptr) {
if (const WaylandOutput* output = resolveEffectiveOutput(*m_wayland, instance.state.outputName); output != nullptr) {
if (const WaylandOutput* output = resolveEffectiveOutput(*m_wayland, instance.state.outputName);
output != nullptr) {
const WidgetTransformClampResult clamped = clampWidgetCenterToOutput(
instance.state.cx, instance.state.cy, instance.intrinsicWidth, instance.intrinsicHeight, instance.state.scale,
instance.state.rotationRad, outputLogicalWidth(*output), outputLogicalHeight(*output),
@@ -302,9 +303,9 @@ void DesktopWidgetsHost::prepareFrame(DesktopWidgetInstance& instance, bool need
}
}
const WidgetTransformSurfaceGeometry geometry = computeWidgetSurfaceGeometry(
instance.state.cx, instance.state.cy, instance.intrinsicWidth, instance.intrinsicHeight, instance.state.scale,
instance.state.rotationRad);
const WidgetTransformSurfaceGeometry geometry =
computeWidgetSurfaceGeometry(instance.state.cx, instance.state.cy, instance.intrinsicWidth,
instance.intrinsicHeight, instance.state.scale, instance.state.rotationRad);
if (instance.surface->width() != geometry.surfaceWidth || instance.surface->height() != geometry.surfaceHeight) {
instance.surface->requestSize(geometry.surfaceWidth, geometry.surfaceHeight);
@@ -317,9 +318,9 @@ void DesktopWidgetsHost::prepareFrame(DesktopWidgetInstance& instance, bool need
}
if (instance.transformNode != nullptr) {
instance.transformNode->setFrameSize(instance.intrinsicWidth, instance.intrinsicHeight);
instance.transformNode->setPosition((static_cast<float>(instance.surface->width()) - instance.intrinsicWidth) * 0.5f,
(static_cast<float>(instance.surface->height()) - instance.intrinsicHeight) *
0.5f);
instance.transformNode->setPosition(
(static_cast<float>(instance.surface->width()) - instance.intrinsicWidth) * 0.5f,
(static_cast<float>(instance.surface->height()) - instance.intrinsicHeight) * 0.5f);
instance.transformNode->setRotation(instance.state.rotationRad);
instance.transformNode->setScale(instance.state.scale);
}
+1 -1
View File
@@ -1,7 +1,7 @@
#pragma once
#include "render/scene/node.h"
#include "render/animation/animation_manager.h"
#include "render/scene/node.h"
#include "shell/desktop/desktop_widget_factory.h"
#include "shell/desktop/desktop_widgets_controller.h"
#include "wayland/layer_surface.h"
+2 -4
View File
@@ -51,10 +51,8 @@ inline WidgetTransformSurfaceGeometry computeWidgetSurfaceGeometry(float cx, flo
WidgetTransformSurfaceGeometry geometry;
geometry.surfaceWidth = std::max<std::uint32_t>(1, static_cast<std::uint32_t>(std::ceil(bounds.aabbWidth)));
geometry.surfaceHeight = std::max<std::uint32_t>(1, static_cast<std::uint32_t>(std::ceil(bounds.aabbHeight)));
geometry.marginLeft =
static_cast<std::int32_t>(std::lround(cx - static_cast<float>(geometry.surfaceWidth) * 0.5f));
geometry.marginTop =
static_cast<std::int32_t>(std::lround(cy - static_cast<float>(geometry.surfaceHeight) * 0.5f));
geometry.marginLeft = static_cast<std::int32_t>(std::lround(cx - static_cast<float>(geometry.surfaceWidth) * 0.5f));
geometry.marginTop = static_cast<std::int32_t>(std::lround(cy - static_cast<float>(geometry.surfaceHeight) * 0.5f));
return geometry;
}
@@ -11,7 +11,8 @@
DesktopAudioVisualizerWidget::DesktopAudioVisualizerWidget(PipeWireSpectrum* spectrum, float width, float height,
int bands)
: m_spectrum(spectrum), m_width(std::max(1.0f, width)), m_height(std::max(1.0f, height)), m_bands(std::max(1, bands)) {}
: m_spectrum(spectrum), m_width(std::max(1.0f, width)), m_height(std::max(1.0f, height)),
m_bands(std::max(1, bands)) {}
DesktopAudioVisualizerWidget::~DesktopAudioVisualizerWidget() {
if (m_spectrum != nullptr && m_listenerId != 0) {
@@ -11,10 +11,10 @@
namespace {
bool formatShowsSeconds(const std::string& format) {
return format.find("%S") != std::string::npos || format.find("%T") != std::string::npos ||
format.find("%X") != std::string::npos;
}
bool formatShowsSeconds(const std::string& format) {
return format.find("%S") != std::string::npos || format.find("%T") != std::string::npos ||
format.find("%X") != std::string::npos;
}
} // namespace
+348 -342
View File
File diff suppressed because it is too large Load Diff
+5 -7
View File
@@ -109,20 +109,18 @@ private:
void launchEntry(const DesktopEntry& entry);
void launchAction(const DesktopAction& action);
void handleItemClick(DockInstance& instance, DockItemView& item);
void openWindowPicker(DockInstance& instance, DockItemView& item,
std::vector<ToplevelInfo> windows);
void openWindowPicker(DockInstance& instance, DockItemView& item, std::vector<ToplevelInfo> windows);
void closeWindowPicker();
void openItemMenu(DockInstance& instance, DockItemView& item);
void closeItemMenu();
void startHideFadeOut(DockInstance& inst);
[[nodiscard]] bool matchesActiveApp(const DockItemView& item, std::string_view activeAppIdLower) const;
[[nodiscard]] bool matchesRunningApp(const DockItemView& item,
const std::vector<std::string>& runningLower) const;
[[nodiscard]] bool matchesRunningApp(const DockItemView& item, const std::vector<std::string>& runningLower) const;
// Geometry helpers
[[nodiscard]] std::int32_t dockContentSize(std::size_t itemCount) const; // item row length (main axis)
[[nodiscard]] std::int32_t dockThickness() const; // cross-axis (includes icon, cell padding, dock padding)
[[nodiscard]] std::int32_t dockThickness() const; // cross-axis (includes icon, cell padding, dock padding)
[[nodiscard]] bool isVertical() const;
// Generic popup (window picker and item context menu).
@@ -151,6 +149,6 @@ private:
std::unordered_map<wl_surface*, DockInstance*> m_surfaceMap;
DockInstance* m_hoveredInstance = nullptr;
DockInstance* m_popupOwnerInstance = nullptr; // instance that owns the current open popup
std::unique_ptr<DockPopup> m_windowMenu; // left-click multi-window picker
std::unique_ptr<DockPopup> m_itemMenu; // right-click context menu
std::unique_ptr<DockPopup> m_windowMenu; // left-click multi-window picker
std::unique_ptr<DockPopup> m_itemMenu; // right-click context menu
};
+1 -1
View File
@@ -1,7 +1,7 @@
#include "shell/launcher/launcher_panel.h"
#include "core/deferred_call.h"
#include "config/config_service.h"
#include "core/deferred_call.h"
#include "core/ui_phase.h"
#include "render/core/renderer.h"
#include "render/scene/input_area.h"

Some files were not shown because too many files have changed in this diff Show More