Merge branch 'noctalia-dev:v5' into v5

This commit is contained in:
Mathew
2026-05-06 21:14:06 -04:00
committed by GitHub
35 changed files with 644 additions and 155 deletions
+5 -3
View File
@@ -23,12 +23,12 @@ jobs:
const extractValue = (heading) => {
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = body.match(new RegExp(`### ${escapedHeading}\\s*\\n+([^\\n\\r]+)`, "i"));
const match = body.match(new RegExp(`^\\s*(?:###\\s*)?${escapedHeading}\\s*\\r?\\n+([^\\n\\r]+)`, "im"));
return match ? match[1].trim() : null;
};
const compositorValue = extractValue("Desktop environment / compositor");
const distributionValue = extractValue("Distribution family");
const compositorValue = extractValue("Compositor");
const distributionValue = extractValue("Distribution");
const compositorLabelMap = {
"Niri": "compositor:niri",
@@ -36,6 +36,7 @@ jobs:
"Sway": "compositor:sway",
"Scroll": "compositor:scroll",
"Labwc": "compositor:labwc",
"Mango": "compositor:mango",
"MangoWC": "compositor:mango",
"Other": "compositor:other"
};
@@ -47,6 +48,7 @@ jobs:
"NixOS": "distro:nixos",
"openSUSE-based": "distro:opensuse",
"Gentoo-based": "distro:gentoo",
"Void": "distro:void",
"Void-based": "distro:void",
"Other": "distro:other"
};
+14
View File
@@ -831,6 +831,14 @@
}
},
"widgets": {
"instances": {
"cpu": "CPU",
"date": "Date",
"input-volume": "Input Volume",
"output-volume": "Output Volume",
"ram": "RAM",
"temp": "Temperature"
},
"categories": {
"custom": "Custom",
"info": "Info",
@@ -883,9 +891,11 @@
"gauge": "Gauge",
"graph": "Graph",
"id": "ID",
"input": "Input",
"name": "Name",
"none": "None",
"on-hover": "On Hover",
"output": "Output",
"ram-percent": "RAM Percent",
"ram-used": "RAM Used",
"short": "Short",
@@ -946,6 +956,10 @@
"label": "Cycle Command",
"description": "Command run when cycling keyboard layouts"
},
"device": {
"label": "Device",
"description": "Audio stream to control"
},
"display": {
"label": "Display",
"description": "Display mode for this widget"
+4
View File
@@ -270,3 +270,7 @@ battery_low_percent_threshold = 0 # set to e.g. 15 to enable battery_under_thr
# [widget.notifications]
# hide_when_no_unread = true
# [widget.clock]
# format = "{:%H:%M}\n{:%d/%m}"
# vertical_format = "{:%H\n%M}"
+2
View File
@@ -390,6 +390,7 @@ _noctalia_sources = files(
'src/render/programs/image_program.cpp',
'src/render/programs/linear_gradient_program.cpp',
'src/render/programs/rect_program.cpp',
'src/render/programs/screen_corner_program.cpp',
'src/render/programs/spinner_program.cpp',
'src/render/programs/audio_spectrum_program.cpp',
'src/render/programs/effect_program.cpp',
@@ -552,6 +553,7 @@ _noctalia_sources = files(
'src/ui/controls/radio_button.cpp',
'src/ui/controls/scroll_view.cpp',
'src/ui/controls/search_picker.cpp',
'src/ui/controls/screen_corner.cpp',
'src/ui/controls/segmented.cpp',
'src/ui/controls/select.cpp',
'src/ui/controls/separator.cpp',
+7
View File
@@ -819,6 +819,13 @@ void Application::initUi() {
// Panel manager must be before bar so widgets can access PanelManager::instance()
m_panelManager.initialize(m_wayland, &m_configService, &m_renderContext);
m_panelManager.setOpenSettingsWindowCallback([this]() { m_settingsWindow.open(); });
m_panelManager.setToggleSettingsWindowCallback([this]() {
if (m_settingsWindow.isOpen()) {
m_settingsWindow.close();
return;
}
m_settingsWindow.open();
});
auto clipboardPanel = std::make_unique<ClipboardPanel>(&m_clipboardService, &m_configService, &m_thumbnailService);
clipboardPanel->setActivateCallback([this](const ClipboardEntry& entry) {
m_panelManager.close();
@@ -612,6 +612,7 @@ bool NiriWorkspaceBackend::handleWindowLayoutsChanged(const nlohmann::json& payl
return false;
}
bool changed = false;
for (const auto& item : *changes) {
if (!item.is_array() || item.size() < 2) {
continue;
@@ -633,6 +634,7 @@ bool NiriWorkspaceBackend::handleWindowLayoutsChanged(const nlohmann::json& payl
if (it->second.x != x || it->second.y != y) {
it->second.x = x;
it->second.y = y;
changed = true;
}
}
}
@@ -640,7 +642,7 @@ bool NiriWorkspaceBackend::handleWindowLayoutsChanged(const nlohmann::json& payl
// Layout positions are useful when a taskbar asks for ordering, but they do
// not affect workspace occupancy or the normal workspace indicators.
return false;
return changed;
}
bool NiriWorkspaceBackend::handleWindowClosed(const nlohmann::json& payload) {
+10
View File
@@ -729,6 +729,16 @@ void ConfigService::seedBuiltinWidgets(Config& config) {
ram.settings["stat"] = std::string("ram_used");
seed("ram", std::move(ram));
WidgetConfig outputVolume;
outputVolume.type = "volume";
outputVolume.settings["device"] = std::string("output");
seed("output_volume", std::move(outputVolume));
WidgetConfig inputVolume;
inputVolume.type = "volume";
inputVolume.settings["device"] = std::string("input");
seed("input_volume", std::move(inputVolume));
WidgetConfig date;
date.type = "clock";
date.settings["format"] = std::string("{:%a %d %b}");
@@ -340,6 +340,13 @@ void GlesRenderBackend::drawSpinner(float surfaceWidth, float surfaceHeight, flo
m_spinnerProgram.draw(surfaceWidth, surfaceHeight, width, height, style, transform);
}
void GlesRenderBackend::drawScreenCorner(float surfaceWidth, float surfaceHeight, float pixelScaleX, float pixelScaleY,
float width, float height, const ScreenCornerStyle& style,
const Mat3& transform) {
m_screenCornerProgram.ensureInitialized();
m_screenCornerProgram.draw(surfaceWidth, surfaceHeight, pixelScaleX, pixelScaleY, width, height, style, transform);
}
void GlesRenderBackend::drawAudioSpectrum(float surfaceWidth, float surfaceHeight, float pixelScaleX, float pixelScaleY,
float width, float height, const AudioSpectrumStyle& style,
std::span<const float> values, const Mat3& transform) {
@@ -429,6 +436,7 @@ void GlesRenderBackend::cleanup() {
m_imageProgram.destroy();
m_glyphProgram.destroy();
m_spinnerProgram.destroy();
m_screenCornerProgram.destroy();
m_audioSpectrumProgram.destroy();
m_effectProgram.destroy();
m_graphProgram.destroy();
+4
View File
@@ -10,6 +10,7 @@
#include "render/programs/graph_program.h"
#include "render/programs/image_program.h"
#include "render/programs/rect_program.h"
#include "render/programs/screen_corner_program.h"
#include "render/programs/spinner_program.h"
#include "render/programs/wallpaper_program.h"
@@ -48,6 +49,8 @@ public:
void drawGlyph(const RenderGlyphDraw& draw) override;
void drawSpinner(float surfaceWidth, float surfaceHeight, float width, float height, const SpinnerStyle& style,
const Mat3& transform) override;
void drawScreenCorner(float surfaceWidth, float surfaceHeight, float pixelScaleX, float pixelScaleY, float width,
float height, const ScreenCornerStyle& style, const Mat3& transform) override;
void drawAudioSpectrum(float surfaceWidth, float surfaceHeight, float pixelScaleX, float pixelScaleY, float width,
float height, const AudioSpectrumStyle& style, std::span<const float> values,
const Mat3& transform) override;
@@ -82,6 +85,7 @@ private:
ImageProgram m_imageProgram;
GlyphProgram m_glyphProgram;
SpinnerProgram m_spinnerProgram;
ScreenCornerProgram m_screenCornerProgram;
AudioSpectrumProgram m_audioSpectrumProgram;
EffectProgram m_effectProgram;
GraphProgram m_graphProgram;
+3
View File
@@ -18,6 +18,7 @@ struct AudioSpectrumStyle;
struct EffectStyle;
struct GraphStyle;
struct RoundedRectStyle;
struct ScreenCornerStyle;
struct SpinnerStyle;
struct TransitionParams;
@@ -122,6 +123,8 @@ public:
virtual void drawGlyph(const RenderGlyphDraw& draw) = 0;
virtual void drawSpinner(float surfaceWidth, float surfaceHeight, float width, float height,
const SpinnerStyle& style, const Mat3& transform) = 0;
virtual void drawScreenCorner(float surfaceWidth, float surfaceHeight, float pixelScaleX, float pixelScaleY,
float width, float height, const ScreenCornerStyle& style, const Mat3& transform) = 0;
virtual void drawAudioSpectrum(float surfaceWidth, float surfaceHeight, float pixelScaleX, float pixelScaleY,
float width, float height, const AudioSpectrumStyle& style,
std::span<const float> values, const Mat3& transform) = 0;
+19
View File
@@ -117,6 +117,25 @@ struct SpinnerStyle {
float thickness = 2.0f;
};
enum class ScreenCornerPosition : std::uint8_t {
TopLeft,
TopRight,
BottomRight,
BottomLeft,
};
struct ScreenCornerStyle {
Color color = rgba(0.0f, 0.0f, 0.0f, 1.0f);
ScreenCornerPosition position = ScreenCornerPosition::TopLeft;
float exponent = 4.0f;
float softness = 1.0f;
};
constexpr bool operator==(const ScreenCornerStyle& lhs, const ScreenCornerStyle& rhs) noexcept {
return lhs.color == rhs.color && lhs.position == rhs.position && lhs.exponent == rhs.exponent &&
lhs.softness == rhs.softness;
}
enum class AudioSpectrumOrientation : std::uint8_t {
Horizontal,
Vertical,
@@ -0,0 +1,131 @@
#include "render/programs/screen_corner_program.h"
#include <algorithm>
#include <array>
#include <stdexcept>
namespace {
constexpr char kVertexShaderSource[] = R"(
precision highp float;
attribute vec2 a_position;
uniform vec2 u_surface_size;
uniform vec2 u_size;
uniform mat3 u_transform;
varying vec2 v_local;
vec2 to_ndc(vec2 pixel_pos) {
vec2 normalized = pixel_pos / u_surface_size;
return vec2(normalized.x * 2.0 - 1.0, 1.0 - normalized.y * 2.0);
}
void main() {
vec2 local = a_position * u_size;
vec3 pixel = u_transform * vec3(local, 1.0);
v_local = local;
gl_Position = vec4(to_ndc(pixel.xy), 0.0, 1.0);
}
)";
constexpr char kFragmentShaderSource[] = R"(
precision highp float;
uniform vec2 u_size;
uniform vec2 u_pixel_scale;
uniform vec4 u_color;
uniform int u_corner;
uniform float u_exponent;
uniform float u_softness;
varying vec2 v_local;
vec2 corner_center() {
if (u_corner == 1) {
return vec2(0.0, u_size.y);
}
if (u_corner == 2) {
return vec2(0.0, 0.0);
}
if (u_corner == 3) {
return vec2(u_size.x, 0.0);
}
return u_size;
}
void main() {
vec2 radius = max(u_size, vec2(1.0));
vec2 normalized = abs(v_local - corner_center()) / radius;
float shape = pow(normalized.x, u_exponent) + pow(normalized.y, u_exponent) - 1.0;
float pixel_scale = max(min(u_pixel_scale.x, u_pixel_scale.y), 1.0);
float aa = max(u_softness / (min(radius.x, radius.y) * pixel_scale), 0.0001);
float coverage = smoothstep(-aa, aa, shape);
float alpha = u_color.a * coverage;
if (alpha <= 0.0) {
discard;
}
gl_FragColor = vec4(u_color.rgb * alpha, alpha);
}
)";
} // namespace
void ScreenCornerProgram::ensureInitialized() {
if (m_program.isValid()) {
return;
}
m_program.create(kVertexShaderSource, kFragmentShaderSource);
m_positionLocation = glGetAttribLocation(m_program.id(), "a_position");
m_surfaceSizeLocation = glGetUniformLocation(m_program.id(), "u_surface_size");
m_sizeLocation = glGetUniformLocation(m_program.id(), "u_size");
m_pixelScaleLocation = glGetUniformLocation(m_program.id(), "u_pixel_scale");
m_colorLocation = glGetUniformLocation(m_program.id(), "u_color");
m_cornerLocation = glGetUniformLocation(m_program.id(), "u_corner");
m_exponentLocation = glGetUniformLocation(m_program.id(), "u_exponent");
m_softnessLocation = glGetUniformLocation(m_program.id(), "u_softness");
m_transformLocation = glGetUniformLocation(m_program.id(), "u_transform");
if (m_positionLocation < 0 || m_surfaceSizeLocation < 0 || m_sizeLocation < 0 || m_pixelScaleLocation < 0 ||
m_colorLocation < 0 || m_cornerLocation < 0 || m_exponentLocation < 0 || m_softnessLocation < 0 ||
m_transformLocation < 0) {
throw std::runtime_error("failed to query screen-corner shader locations");
}
}
void ScreenCornerProgram::destroy() {
m_program.destroy();
m_positionLocation = -1;
m_surfaceSizeLocation = -1;
m_sizeLocation = -1;
m_pixelScaleLocation = -1;
m_colorLocation = -1;
m_cornerLocation = -1;
m_exponentLocation = -1;
m_softnessLocation = -1;
m_transformLocation = -1;
}
void ScreenCornerProgram::draw(float surfaceWidth, float surfaceHeight, float pixelScaleX, float pixelScaleY,
float width, float height, const ScreenCornerStyle& style, const Mat3& transform) const {
if (!m_program.isValid() || width <= 0.0f || height <= 0.0f || style.color.a <= 0.0f) {
return;
}
const std::array<GLfloat, 12> vertices = {
0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
};
glUseProgram(m_program.id());
glUniform2f(m_surfaceSizeLocation, surfaceWidth, surfaceHeight);
glUniform2f(m_sizeLocation, width, height);
glUniform2f(m_pixelScaleLocation, std::max(1.0f, pixelScaleX), std::max(1.0f, pixelScaleY));
glUniform4f(m_colorLocation, style.color.r, style.color.g, style.color.b, style.color.a);
glUniform1i(m_cornerLocation, static_cast<GLint>(style.position));
glUniform1f(m_exponentLocation, std::max(1.0f, style.exponent));
glUniform1f(m_softnessLocation, std::max(0.0f, style.softness));
glUniformMatrix3fv(m_transformLocation, 1, GL_FALSE, transform.m.data());
glVertexAttribPointer(m_positionLocation, 2, GL_FLOAT, GL_FALSE, 0, vertices.data());
glEnableVertexAttribArray(m_positionLocation);
glDrawArrays(GL_TRIANGLES, 0, 6);
glDisableVertexAttribArray(m_positionLocation);
}
@@ -0,0 +1,34 @@
#pragma once
#include "render/core/mat3.h"
#include "render/core/render_styles.h"
#include "render/core/shader_program.h"
#include <GLES2/gl2.h>
class ScreenCornerProgram {
public:
ScreenCornerProgram() = default;
~ScreenCornerProgram() = default;
ScreenCornerProgram(const ScreenCornerProgram&) = delete;
ScreenCornerProgram& operator=(const ScreenCornerProgram&) = delete;
void ensureInitialized();
void destroy();
void draw(float surfaceWidth, float surfaceHeight, float pixelScaleX, float pixelScaleY, float width, float height,
const ScreenCornerStyle& style, const Mat3& transform = Mat3::identity()) const;
private:
ShaderProgram m_program;
GLint m_positionLocation = -1;
GLint m_surfaceSizeLocation = -1;
GLint m_sizeLocation = -1;
GLint m_pixelScaleLocation = -1;
GLint m_colorLocation = -1;
GLint m_cornerLocation = -1;
GLint m_exponentLocation = -1;
GLint m_softnessLocation = -1;
GLint m_transformLocation = -1;
};
+10
View File
@@ -15,6 +15,7 @@
#include "render/scene/image_node.h"
#include "render/scene/node.h"
#include "render/scene/rect_node.h"
#include "render/scene/screen_corner_node.h"
#include "render/scene/spinner_node.h"
#include "render/scene/text_node.h"
#include "render/scene/wallpaper_node.h"
@@ -320,6 +321,15 @@ void RenderContext::renderNode(const Node* node, const Mat3& parentTransform, fl
m_backend->drawSpinner(sw, sh, node->width(), node->height(), style, worldTransform);
break;
}
case NodeType::ScreenCorner: {
const auto* corner = static_cast<const ScreenCornerNode*>(node);
auto style = corner->style();
style.color.a *= effectiveOpacity;
const float pixelScaleX = sw > 0.0f ? bw / sw : 1.0f;
const float pixelScaleY = sh > 0.0f ? bh / sh : 1.0f;
m_backend->drawScreenCorner(sw, sh, pixelScaleX, pixelScaleY, node->width(), node->height(), style, worldTransform);
break;
}
case NodeType::AudioSpectrum: {
const auto* spectrum = static_cast<const AudioSpectrumNode*>(node);
auto style = spectrum->style();
+1
View File
@@ -17,6 +17,7 @@ enum class NodeType : std::uint8_t {
Image,
Glyph,
Spinner,
ScreenCorner,
AudioSpectrum,
Effect,
Graph,
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include "render/core/render_styles.h"
#include "render/scene/node.h"
class ScreenCornerNode : public Node {
public:
ScreenCornerNode() : Node(NodeType::ScreenCorner) {}
[[nodiscard]] const ScreenCornerStyle& style() const noexcept { return m_style; }
void setStyle(const ScreenCornerStyle& style) {
if (m_style == style) {
return;
}
m_style = style;
markPaintDirty();
}
void setColor(const Color& color) {
if (m_style.color == color) {
return;
}
m_style.color = color;
markPaintDirty();
}
void setCorner(ScreenCornerPosition position) {
if (m_style.position == position) {
return;
}
m_style.position = position;
markPaintDirty();
}
void setExponent(float exponent) {
if (m_style.exponent == exponent) {
return;
}
m_style.exponent = exponent;
markPaintDirty();
}
void setSoftness(float softness) {
if (m_style.softness == softness) {
return;
}
m_style.softness = softness;
markPaintDirty();
}
private:
ScreenCornerStyle m_style;
};
+3 -1
View File
@@ -356,7 +356,9 @@ std::unique_ptr<Widget> WidgetFactory::create(const std::string& name, wl_output
if (type == "volume") {
const bool showLabel = wc != nullptr ? wc->getBool("show_label", true) : true;
auto widget = std::make_unique<VolumeWidget>(m_audio, output, showLabel);
const std::string target = wc != nullptr ? wc->getString("device", "output") : std::string("output");
const auto volumeTarget = target == "input" ? VolumeWidgetTarget::Input : VolumeWidgetTarget::Output;
auto widget = std::make_unique<VolumeWidget>(m_audio, output, showLabel, volumeTarget);
widget->setContentScale(contentScale);
return widget;
}
+101 -8
View File
@@ -8,6 +8,24 @@
#include "ui/palette.h"
#include "ui/style.h"
#include <algorithm>
#include <cmath>
namespace {
constexpr float kStackedPrimaryScale = 0.72f;
constexpr float kStackedSecondaryScale = 0.62f;
constexpr float kStackedPrimaryScaleNoCapsule = 0.80f;
constexpr float kStackedSecondaryScaleNoCapsule = 0.72f;
std::pair<std::string_view, std::string_view> splitFirstLine(std::string_view text) {
const std::size_t newline = text.find('\n');
if (newline == std::string_view::npos) {
return {text, {}};
}
return {text.substr(0, newline), text.substr(newline + 1)};
}
} // namespace
ClockWidget::ClockWidget(wl_output* output, std::string format, std::string verticalFormat)
: m_output(output), m_format(std::move(format)), m_verticalFormat(std::move(verticalFormat)) {}
@@ -60,36 +78,111 @@ void ClockWidget::create() {
// a stable ink envelope instead of the current text's ink.
m_label = label.get();
area->addChild(std::move(label));
auto secondaryLabel = std::make_unique<Label>();
secondaryLabel->setBold(false);
secondaryLabel->setTextAlign(TextAlign::Center);
secondaryLabel->setFontSize(Style::fontSizeBody * m_contentScale * kStackedSecondaryScale);
secondaryLabel->setVisible(false);
m_secondaryLabel = secondaryLabel.get();
area->addChild(std::move(secondaryLabel));
setRoot(std::move(area));
}
void ClockWidget::doLayout(Renderer& renderer, float containerWidth, float containerHeight) {
auto* rootNode = root();
if (m_label == nullptr || rootNode == nullptr) {
if (m_label == nullptr || m_secondaryLabel == nullptr || rootNode == nullptr) {
return;
}
m_isVertical = containerHeight > containerWidth;
update(renderer);
m_label->setColor(widgetForegroundOr(colorSpecFromRole(ColorRole::OnSurface)));
// Horizontal clocks should use single-line metrics so capsule height matches sibling widgets.
const ColorSpec foreground = widgetForegroundOr(colorSpecFromRole(ColorRole::OnSurface));
m_label->setColor(foreground);
m_secondaryLabel->setColor(foreground);
const bool showSecondary = !m_isVertical && !m_lastSecondaryText.empty();
const bool noCapsule = !barCapsuleSpec().enabled;
const float stackedPrimaryScale = noCapsule ? kStackedPrimaryScaleNoCapsule : kStackedPrimaryScale;
const float stackedSecondaryScale = noCapsule ? kStackedSecondaryScaleNoCapsule : kStackedSecondaryScale;
float primaryFontSize = Style::fontSizeBody * m_contentScale * (showSecondary ? stackedPrimaryScale : 1.0f);
float secondaryFontSize = Style::fontSizeBody * m_contentScale * stackedSecondaryScale;
// Horizontal clocks use single-line metrics unless the configured format
// explicitly contains line breaks.
m_label->setFontSize(primaryFontSize);
m_label->setBold(true);
m_secondaryLabel->setBold(true);
m_secondaryLabel->setFontSize(secondaryFontSize);
m_label->setMaxLines(m_isVertical ? 0 : 1);
m_label->setMinWidth(0.0f);
m_label->setMaxWidth(m_isVertical ? containerWidth : 0.0f);
m_label->measure(renderer);
m_label->setPosition(0.0f, 0.0f);
rootNode->setSize(m_label->width(), m_label->height());
m_secondaryLabel->setVisible(showSecondary);
m_secondaryLabel->setMaxLines(0);
m_secondaryLabel->setMinWidth(0.0f);
m_secondaryLabel->setMaxWidth(0.0f);
if (showSecondary) {
m_secondaryLabel->measure(renderer);
}
float width = showSecondary ? std::max(m_label->width(), m_secondaryLabel->width()) : m_label->width();
float height = showSecondary ? m_label->height() + m_secondaryLabel->height() : m_label->height();
if (!m_isVertical && showSecondary && containerHeight > 0.0f && height > containerHeight) {
const float fitScale = std::min(containerHeight / height, 1.0f);
primaryFontSize *= fitScale;
secondaryFontSize *= fitScale;
m_label->setFontSize(primaryFontSize);
m_secondaryLabel->setFontSize(secondaryFontSize);
m_label->measure(renderer);
m_secondaryLabel->measure(renderer);
width = std::max(m_label->width(), m_secondaryLabel->width());
height = m_label->height() + m_secondaryLabel->height();
}
if (showSecondary) {
const auto primaryMetrics =
renderer.measureText(m_lastPrimaryText, primaryFontSize, true, 0.0f, 1, TextAlign::Start);
const auto secondaryMetrics =
renderer.measureText(m_lastSecondaryText, secondaryFontSize, false, 0.0f, 1, TextAlign::Start);
const float centerX = width * 0.5f;
const float primaryInkCenterX = (primaryMetrics.inkLeft + primaryMetrics.inkRight) * 0.5f;
const float secondaryInkCenterX = (secondaryMetrics.inkLeft + secondaryMetrics.inkRight) * 0.5f;
m_label->setPosition(std::round(centerX - primaryInkCenterX), 0.0f);
m_secondaryLabel->setPosition(std::round(centerX - secondaryInkCenterX), m_label->height());
} else {
m_label->setPosition(0.0f, 0.0f);
}
rootNode->setSize(width, height);
}
void ClockWidget::doUpdate(Renderer& renderer) {
if (m_label == nullptr) {
if (m_label == nullptr || m_secondaryLabel == nullptr) {
return;
}
auto text = formatTimeText();
if (text != m_lastText) {
m_lastText = std::move(text);
m_label->setText(m_lastText);
}
std::string primaryText = m_lastText;
std::string secondaryText;
if (!m_isVertical) {
const auto [primary, secondary] = splitFirstLine(m_lastText);
primaryText = std::string(primary);
secondaryText = std::string(secondary);
}
if (primaryText != m_lastPrimaryText) {
m_lastPrimaryText = std::move(primaryText);
m_label->setText(m_lastPrimaryText);
m_label->measure(renderer);
}
if (secondaryText != m_lastSecondaryText) {
m_lastSecondaryText = std::move(secondaryText);
m_secondaryLabel->setText(m_lastSecondaryText);
m_secondaryLabel->measure(renderer);
}
}
+3
View File
@@ -23,5 +23,8 @@ private:
std::string m_verticalFormat;
bool m_isVertical = false;
Label* m_label = nullptr;
Label* m_secondaryLabel = nullptr;
std::string m_lastText;
std::string m_lastPrimaryText;
std::string m_lastSecondaryText;
};
+4 -66
View File
@@ -791,71 +791,6 @@ void TaskbarWidget::updateModels() {
}
(void)tryClaim(false, false);
}
std::vector<bool> representedAssignments(workspaceAssignments.size(), false);
for (const auto& task : nextTasks) {
if (task.workspaceKey.empty()) {
continue;
}
for (std::size_t i = 0; i < workspaceAssignments.size(); ++i) {
if (representedAssignments[i]) {
continue;
}
const auto& assignment = workspaceAssignments[i];
const std::string assignmentAppLower = toLower(assignment.appId);
const bool appMatches = assignmentAppLower == task.appIdLower || assignmentAppLower == task.idLower ||
assignmentAppLower == task.startupWmClassLower ||
assignmentAppLower == task.nameLower;
if (!appMatches || assignment.workspaceKey != task.workspaceKey) {
continue;
}
if (!task.workspaceWindowId.empty() && assignment.windowId != task.workspaceWindowId) {
continue;
}
if (!task.title.empty() && !assignment.title.empty() && assignment.title != task.title) {
continue;
}
representedAssignments[i] = true;
break;
}
}
auto syntheticTaskKey = [](const WorkspaceWindowAssignment& assignment, std::size_t index) {
const std::string seed = assignment.windowId.empty()
? assignment.workspaceKey + "\n" + assignment.appId + "\n" + assignment.title +
"\n" + std::to_string(index)
: assignment.windowId;
std::uintptr_t value = static_cast<std::uintptr_t>(std::hash<std::string>{}(seed));
if (value == 0) {
value = static_cast<std::uintptr_t>(index + 1);
}
return value;
};
for (std::size_t i = 0; i < workspaceAssignments.size(); ++i) {
if (representedAssignments[i]) {
continue;
}
const auto& assignment = workspaceAssignments[i];
if (assignment.workspaceKey.empty() || assignment.appId.empty()) {
continue;
}
TaskModel task{};
task.handleKey = syntheticTaskKey(assignment, i);
task.order = static_cast<std::uint64_t>(std::numeric_limits<std::int32_t>::max()) + i;
task.appId = assignment.appId;
task.idLower = toLower(task.appId);
task.startupWmClassLower = task.idLower;
task.nameLower = task.idLower;
task.appIdLower = task.idLower;
task.title = assignment.title;
task.iconPath = resolveIconPath(task.appId, {});
task.workspaceKey = assignment.workspaceKey;
task.workspaceWindowId = assignment.windowId;
task.workspaceOrder = i;
nextTasks.push_back(std::move(task));
}
}
}
@@ -921,7 +856,10 @@ void TaskbarWidget::updateModels() {
previousWorkspaceByHandle[task.handleKey] = task.workspaceKey;
}
}
if (assignmentMode != TaskbarAssignmentMode::WorkspaceOccurrenceTitle) {
const bool hasStableWorkspaceWindowAssignments =
std::any_of(workspaceAssignments.begin(), workspaceAssignments.end(),
[](const WorkspaceWindowAssignment& assignment) { return !assignment.windowId.empty(); });
if (assignmentMode != TaskbarAssignmentMode::WorkspaceOccurrenceTitle && !hasStableWorkspaceWindowAssignments) {
std::unordered_set<std::uintptr_t> seenHandles;
seenHandles.reserve(nextTasks.size());
for (auto& task : nextTasks) {
+19 -11
View File
@@ -15,7 +15,11 @@
namespace {
const char* volumeGlyphName(float volume, bool muted) {
const char* volumeGlyphName(float volume, bool muted, VolumeWidgetTarget target) {
if (target == VolumeWidgetTarget::Input) {
return muted ? "microphone-mute" : "microphone";
}
if (muted || volume <= 0.0f) {
return "volume-mute";
}
@@ -29,8 +33,8 @@ namespace {
} // namespace
VolumeWidget::VolumeWidget(PipeWireService* audio, wl_output* output, bool showLabel)
: m_audio(audio), m_output(output), m_showLabel(showLabel) {}
VolumeWidget::VolumeWidget(PipeWireService* audio, wl_output* output, bool showLabel, VolumeWidgetTarget target)
: m_audio(audio), m_output(output), m_showLabel(showLabel), m_target(target) {}
void VolumeWidget::create() {
auto area = std::make_unique<InputArea>();
@@ -39,13 +43,17 @@ void VolumeWidget::create() {
if (m_audio == nullptr) {
return;
}
const auto* sink = m_audio->defaultSink();
if (sink == nullptr) {
const auto* node = m_target == VolumeWidgetTarget::Input ? m_audio->defaultSource() : m_audio->defaultSink();
if (node == nullptr) {
return;
}
const float delta = data.scrollDelta(1.0f) > 0 ? -kScrollStep : kScrollStep;
const float newValue = std::clamp(sink->volume + delta, 0.0f, 1.0f);
m_audio->setSinkVolume(sink->id, newValue);
const float newValue = std::clamp(node->volume + delta, 0.0f, 1.0f);
if (m_target == VolumeWidgetTarget::Input) {
m_audio->setSourceVolume(node->id, newValue);
} else {
m_audio->setSinkVolume(node->id, newValue);
}
});
auto glyph = std::make_unique<Glyph>();
@@ -103,9 +111,9 @@ void VolumeWidget::syncState(Renderer& renderer) {
return;
}
const auto* sink = m_audio->defaultSink();
float volume = sink != nullptr ? sink->volume : 0.0f;
bool muted = sink != nullptr ? sink->muted : false;
const auto* node = m_target == VolumeWidgetTarget::Input ? m_audio->defaultSource() : m_audio->defaultSink();
float volume = node != nullptr ? node->volume : 0.0f;
bool muted = node != nullptr ? node->muted : false;
if (volume == m_lastVolume && muted == m_lastMuted && m_isVertical == m_lastVertical) {
return;
@@ -115,7 +123,7 @@ void VolumeWidget::syncState(Renderer& renderer) {
m_lastMuted = muted;
m_lastVertical = m_isVertical;
m_glyph->setGlyph(volumeGlyphName(volume, muted));
m_glyph->setGlyph(volumeGlyphName(volume, muted, m_target));
m_glyph->setGlyphSize(Style::barGlyphSize * m_contentScale);
m_glyph->setColor(muted ? colorSpecFromRole(ColorRole::OnSurfaceVariant)
: widgetForegroundOr(colorSpecFromRole(ColorRole::OnSurface)));
+7 -1
View File
@@ -9,9 +9,14 @@ class Label;
class PipeWireService;
struct wl_output;
enum class VolumeWidgetTarget {
Output,
Input,
};
class VolumeWidget : public Widget {
public:
VolumeWidget(PipeWireService* audio, wl_output* output, bool showLabel);
VolumeWidget(PipeWireService* audio, wl_output* output, bool showLabel, VolumeWidgetTarget target);
void create() override;
@@ -23,6 +28,7 @@ private:
PipeWireService* m_audio = nullptr;
wl_output* m_output = nullptr;
bool m_showLabel = true;
VolumeWidgetTarget m_target = VolumeWidgetTarget::Output;
Glyph* m_glyph = nullptr;
Label* m_label = nullptr;
float m_lastVolume = -1.0f;
+2 -12
View File
@@ -136,7 +136,6 @@ void WorkspacesWidget::rebuild(Renderer& renderer) {
// Compute each slot's intrinsic label-padded width (if labelled).
std::vector<float> labelPadded(workspaces.size(), 0.0f);
std::vector<float> labelInkCenterX(workspaces.size(), 0.0f);
bool anyMultiChar = false;
bool anyLabel = false;
for (std::size_t i = 0; i < workspaces.size(); ++i) {
@@ -153,11 +152,6 @@ void WorkspacesWidget::rebuild(Renderer& renderer) {
const float inkHeight = std::max(0.0f, tm.inkBottom - tm.inkTop);
indicatorHeight = std::max(indicatorHeight, std::round(std::max(labelRefHeight, inkHeight)));
labelPadded[i] = std::max(pillMin, inkWidth + (kWorkspaceLabelPadH * m_contentScale * 2.0f));
// Visible-ink horizontal center within the label box. Used to center digits
// by their visible glyph rather than by their advance width — for fonts where
// the ink is asymmetric within the advance (e.g. "4"), centering by advance
// looks visibly off.
labelInkCenterX[i] = (tm.inkLeft + tm.inkRight) * 0.5f;
}
const float minCircleExtent = std::round(indicatorHeight);
@@ -204,8 +198,6 @@ void WorkspacesWidget::rebuild(Renderer& renderer) {
item.showLabel = showLabel;
item.inactiveWidth = inactiveWidth;
item.activeWidth = activeWidth;
item.textInkCenterX = labelInkCenterX[i];
auto indicator = std::make_unique<Box>();
indicator->clearBorder();
indicator->setRadius(indicatorHeight * 0.5f);
@@ -359,10 +351,8 @@ void WorkspacesWidget::applyItemLayout(std::size_t i) {
if (it.text != nullptr) {
const float itemW = m_isVertical ? m_indicatorHeight : it.currentWidth;
const float itemH = m_isVertical ? it.currentWidth : m_indicatorHeight;
// Center on the visible ink center, not the advance box center, so digits
// with asymmetric ink (e.g. "4") sit optically centered in the slot.
const float textX = std::round(itemW * 0.5f - it.textInkCenterX);
it.text->setPosition(std::max(0.0f, textX), std::round((itemH - it.text->height()) * 0.5f));
const float textX = std::round((itemW - it.text->width()) * 0.5f);
it.text->setPosition(std::max(0.0f, textX), (itemH - it.text->height()) * 0.5f);
}
if (it.indicator != nullptr) {
const float itemW = m_isVertical ? m_indicatorHeight : it.currentWidth;
@@ -58,7 +58,6 @@ private:
float targetWidth = 0.0f;
float currentX = 0.0f;
float currentWidth = 0.0f;
float textInkCenterX = 0.0f;
};
[[nodiscard]] ColorRole workspaceFillRole(const Workspace& workspace) const;
+3 -1
View File
@@ -681,7 +681,9 @@ void WeatherTab::sync(Renderer& renderer) {
m_tempMinLabel->setText(!snapshot.forecastDays.empty() ? std::format("{}{}", temp, unit) : std::string("--"));
}
if (m_elevationLabel != nullptr) {
m_elevationLabel->setText(std::format("{}m", static_cast<int>(snapshot.elevationM)));
const bool imperial = m_weather->useImperial();
const int elevation = static_cast<int>(imperial ? snapshot.elevationM * 3.28084 : snapshot.elevationM);
m_elevationLabel->setText(std::format("{}{}", elevation, imperial ? "ft" : "m"));
}
if (m_timeZoneLabel != nullptr) {
// Use the last component of the IANA path ("America/Toronto" → "Toronto") to keep
+42 -20
View File
@@ -5,6 +5,7 @@
#include "core/deferred_call.h"
#include "core/log.h"
#include "core/ui_phase.h"
#include "cursor-shape-v1-client-protocol.h"
#include "i18n/i18n.h"
#include "net/http_client.h"
#include "net/uri.h"
@@ -163,6 +164,13 @@ namespace {
float notificationTextMaxWidth() { return std::max(0.0f, kCardWidth - notificationTextStartX() - kCardInnerPad); }
bool isCloseButtonHit(float localX, float localY) {
const float closeLeft = static_cast<float>(kCardWidth) - kCardInnerPad - kCloseButtonSize;
const float closeTop = kCardInnerPad;
return localX >= closeLeft && localX < closeLeft + kCloseButtonSize && localY >= closeTop &&
localY < closeTop + kCloseButtonSize;
}
bool isBlankText(std::string_view text) {
return text.empty() ||
std::all_of(text.begin(), text.end(), [](unsigned char ch) { return std::isspace(ch) != 0; });
@@ -729,25 +737,37 @@ void NotificationToast::addCardToInstance(Instance& inst, std::size_t entryIndex
const uint32_t notificationId = entry.notificationId;
Glyph* closeGlyphPtr = cs.closeGlyph;
ProgressBar* progressBarPtr = cs.progressBar;
InputArea* cardInput = card;
card->setOnEnter(
[this, closeGlyphPtr, closeColorHover, notificationId, progressBarPtr](const InputArea::PointerData&) {
closeGlyphPtr->setColor(closeColorHover);
if (auto* popup = findEntry(notificationId); popup != nullptr) {
popup->hovered = true;
popup->remainingProgress = std::clamp(progressBarPtr->progress(), 0.0f, 1.0f);
}
pauseCountdowns(notificationId);
// Pause the server-side expiry — otherwise NotificationManager's own timer
// would fire Closed behind our back, which is what "the progress bar stops
// but the timer keeps running" was.
if (m_notifications != nullptr) {
m_notifications->pauseExpiry(notificationId);
}
});
card->setOnEnter([this, closeGlyphPtr, closeColorNormal, closeColorHover, notificationId, progressBarPtr,
cardInput](const InputArea::PointerData& data) {
const bool closeHovered = isCloseButtonHit(data.localX, data.localY);
closeGlyphPtr->setColor(closeHovered ? closeColorHover : closeColorNormal);
cardInput->setCursorShape(closeHovered ? WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER
: WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
if (auto* popup = findEntry(notificationId); popup != nullptr) {
popup->hovered = true;
popup->remainingProgress = std::clamp(progressBarPtr->progress(), 0.0f, 1.0f);
}
pauseCountdowns(notificationId);
// Pause the server-side expiry — otherwise NotificationManager's own timer
// would fire Closed behind our back, which is what "the progress bar stops
// but the timer keeps running" was.
if (m_notifications != nullptr) {
m_notifications->pauseExpiry(notificationId);
}
});
card->setOnLeave([this, notificationId, totalDuration, closeGlyphPtr, closeColorNormal, progressBarPtr]() {
card->setOnMotion([closeGlyphPtr, closeColorNormal, closeColorHover, cardInput](const InputArea::PointerData& data) {
const bool closeHovered = isCloseButtonHit(data.localX, data.localY);
closeGlyphPtr->setColor(closeHovered ? closeColorHover : closeColorNormal);
cardInput->setCursorShape(closeHovered ? WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER
: WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
});
card->setOnLeave([this, notificationId, totalDuration, closeGlyphPtr, closeColorNormal, progressBarPtr, cardInput]() {
closeGlyphPtr->setColor(closeColorNormal);
cardInput->setCursorShape(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
if (auto* popup = findEntry(notificationId); popup != nullptr) {
popup->hovered = false;
popup->remainingProgress = std::clamp(progressBarPtr->progress(), 0.0f, 1.0f);
@@ -1464,14 +1484,15 @@ InputArea* NotificationToast::buildCard(const PopupEntry& entry, Node** outCardC
auto viewport = std::make_unique<InputArea>();
viewport->setSize(kCardWidth, cardHeight);
viewport->setClipChildren(true);
// Unified close mechanism: right-clicking anywhere on the card dismisses it. The (X) glyph
// is purely visual — it brightens while the card is hovered via the card's own
// onEnter/onLeave handlers installed in addCardToInstance().
viewport->setAcceptedButtons(BTN_LEFT | BTN_RIGHT);
// Right-clicking anywhere dismisses the card, while the visual (X) keeps its
// familiar left-click close affordance without adding a nested hover target.
viewport->setOnClick([this, id = entry.notificationId](const InputArea::PointerData& data) {
if (data.button == BTN_RIGHT) {
if (data.button == BTN_RIGHT || (data.button == BTN_LEFT && isCloseButtonHit(data.localX, data.localY))) {
removePopup(id);
}
});
viewport->setCursorShape(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT);
auto cardRoot = std::make_unique<Node>();
cardRoot->setSize(kCardWidth, cardHeight);
@@ -1628,6 +1649,7 @@ InputArea* NotificationToast::buildCard(const PopupEntry& entry, Node** outCardC
actionButton->setVariant(ButtonVariant::Outline);
actionButton->setFontSize(Style::fontSizeCaption);
actionButton->setText(actionLabel);
actionButton->setCursorShape(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER);
actionButton->setOnEnter([this, notificationId]() {
pauseCountdowns(notificationId);
if (m_notifications != nullptr) {
+18 -1
View File
@@ -91,6 +91,10 @@ void PanelManager::setOpenSettingsWindowCallback(std::function<void()> callback)
m_openSettingsWindow = std::move(callback);
}
void PanelManager::setToggleSettingsWindowCallback(std::function<void()> callback) {
m_toggleSettingsWindow = std::move(callback);
}
void PanelManager::openSettingsWindow() {
if (isOpen() && !m_closing) {
closePanel();
@@ -100,6 +104,19 @@ void PanelManager::openSettingsWindow() {
}
}
void PanelManager::toggleSettingsWindow() {
if (isOpen() && !m_closing) {
closePanel();
}
if (m_toggleSettingsWindow) {
m_toggleSettingsWindow();
return;
}
if (m_openSettingsWindow) {
m_openSettingsWindow();
}
}
void PanelManager::setAttachedPanelGeometryCallback(
std::function<void(wl_output*, std::optional<AttachedPanelGeometry>)> callback) {
m_attachedPanelGeometryCallback = std::move(callback);
@@ -1545,7 +1562,7 @@ void PanelManager::registerIpc(IpcService& ipc) {
ipc.registerHandler(
"settings-toggle",
[this](const std::string&) -> std::string {
openSettingsWindow();
toggleSettingsWindow();
return "ok\n";
},
"settings-toggle", "Toggle the settings window");
+3
View File
@@ -55,7 +55,9 @@ public:
// Optional: invoked from shell UI (e.g. control center) to spawn the standalone settings toplevel.
void setOpenSettingsWindowCallback(std::function<void()> callback);
void setToggleSettingsWindowCallback(std::function<void()> callback);
void openSettingsWindow();
void toggleSettingsWindow();
void setAttachedPanelGeometryCallback(std::function<void(wl_output*, std::optional<AttachedPanelGeometry>)> callback);
// Callback to query the bar surface rects on a given output, in output-local
// coordinates. The click shield's input region excludes these rects so
@@ -145,6 +147,7 @@ private:
ConfigService* m_config = nullptr;
RenderContext* m_renderContext = nullptr;
std::function<void()> m_openSettingsWindow;
std::function<void()> m_toggleSettingsWindow;
std::function<void(wl_output*, std::optional<AttachedPanelGeometry>)> m_attachedPanelGeometryCallback;
std::function<std::vector<InputRect>(wl_output*)> m_clickShieldExcludeRectsProvider;
std::function<std::vector<wl_surface*>()> m_focusGrabBarSurfacesProvider;
+25 -21
View File
@@ -3,7 +3,7 @@
#include "config/config_service.h"
#include "core/ui_phase.h"
#include "render/render_context.h"
#include "ui/controls/box.h"
#include "ui/controls/screen_corner.h"
#include "wayland/wayland_connection.h"
#include <algorithm>
@@ -17,18 +17,18 @@ namespace {
LayerShellAnchor::Bottom | LayerShellAnchor::Left,
};
Radii cornerRadii(int cornerIndex, float size) {
ScreenCornerPosition cornerPosition(int cornerIndex) {
switch (cornerIndex) {
case 0:
return Radii{size, 0.0f, 0.0f, 0.0f};
return ScreenCornerPosition::TopLeft;
case 1:
return Radii{0.0f, size, 0.0f, 0.0f};
return ScreenCornerPosition::TopRight;
case 2:
return Radii{0.0f, 0.0f, size, 0.0f};
return ScreenCornerPosition::BottomRight;
case 3:
return Radii{0.0f, 0.0f, 0.0f, size};
return ScreenCornerPosition::BottomLeft;
default:
return Radii{};
return ScreenCornerPosition::TopLeft;
}
}
@@ -105,14 +105,16 @@ void ScreenCorners::ensureSurfaces() {
auto* cornerPtr = &corner;
const int cornerIndex = i;
const float cornerSize = static_cast<float>(size);
corner.surface->setConfigureCallback(
[cornerPtr](std::uint32_t, std::uint32_t) { cornerPtr->surface->requestLayout(); });
corner.surface->setPrepareFrameCallback([this, cornerPtr, cornerSize, cornerIndex](bool, bool) {
if (cornerPtr->sceneRoot == nullptr) {
corner.surface->setPrepareFrameCallback([this, cornerPtr, size, cornerIndex](bool, bool) {
auto& target = cornerPtr->surface->renderTarget();
const auto width = target.logicalWidth() == 0 ? size : target.logicalWidth();
const auto height = target.logicalHeight() == 0 ? size : target.logicalHeight();
if (cornerPtr->sceneRoot == nullptr || cornerPtr->builtWidth != width || cornerPtr->builtHeight != height) {
UiPhaseScope layoutPhase(UiPhase::Layout);
buildCornerScene(*cornerPtr, cornerSize, cornerIndex);
buildCornerScene(*cornerPtr, width, height, cornerIndex);
}
});
corner.surface->setRenderContext(m_renderContext);
@@ -132,17 +134,19 @@ void ScreenCorners::ensureSurfaces() {
void ScreenCorners::destroySurfaces() { m_instances.clear(); }
void ScreenCorners::buildCornerScene(Corner& corner, float size, int cornerIndex) {
auto root = std::make_unique<Box>();
root->setSize(size, size);
root->setStyle(RoundedRectStyle{
.fill = Color{0.0f, 0.0f, 0.0f, 1.0f},
.fillMode = FillMode::Solid,
.radius = cornerRadii(cornerIndex, size),
.softness = 0.5f,
.invertFill = true,
});
void ScreenCorners::buildCornerScene(Corner& corner, std::uint32_t width, std::uint32_t height, int cornerIndex) {
const float logicalWidth = static_cast<float>(std::max<std::uint32_t>(1, width));
const float logicalHeight = static_cast<float>(std::max<std::uint32_t>(1, height));
auto root = std::make_unique<ScreenCorner>();
root->setSize(logicalWidth, logicalHeight);
root->setColor(Color{0.0f, 0.0f, 0.0f, 1.0f});
root->setCorner(cornerPosition(cornerIndex));
root->setExponent(4.0f);
root->setSoftness(1.5f);
corner.sceneRoot = std::move(root);
corner.builtWidth = width;
corner.builtHeight = height;
corner.surface->setSceneRoot(corner.sceneRoot.get());
}
+3 -1
View File
@@ -27,6 +27,8 @@ private:
struct Corner {
std::unique_ptr<LayerSurface> surface;
std::unique_ptr<Node> sceneRoot;
std::uint32_t builtWidth = 0;
std::uint32_t builtHeight = 0;
};
struct OutputInstance {
@@ -36,7 +38,7 @@ private:
void ensureSurfaces();
void destroySurfaces();
void buildCornerScene(Corner& corner, float size, int cornerIndex);
void buildCornerScene(Corner& corner, std::uint32_t width, std::uint32_t height, int cornerIndex);
WaylandConnection* m_wayland = nullptr;
ConfigService* m_config = nullptr;
@@ -238,6 +238,28 @@ namespace settings {
return spec;
}
std::string widgetInstanceDisplayLabel(std::string_view name) {
if (name == "cpu") {
return tr("settings.widgets.instances.cpu");
}
if (name == "temp") {
return tr("settings.widgets.instances.temp");
}
if (name == "ram") {
return tr("settings.widgets.instances.ram");
}
if (name == "date") {
return tr("settings.widgets.instances.date");
}
if (name == "output_volume") {
return tr("settings.widgets.instances.output-volume");
}
if (name == "input_volume") {
return tr("settings.widgets.instances.input-volume");
}
return std::string(name);
}
void addPickerEntry(std::vector<WidgetPickerEntry>& entries, std::unordered_set<std::string>& seen,
std::string value, std::string label, std::string description, std::string category,
WidgetReferenceKind kind) {
@@ -317,7 +339,7 @@ namespace settings {
if (const auto it = cfg.widgets.find(std::string(name)); it != cfg.widgets.end()) {
return WidgetReferenceInfo{
.title = std::string(name),
.title = widgetInstanceDisplayLabel(name),
.detail = it->second.type.empty() ? tr("settings.entities.widget.detail.custom")
: tr("settings.entities.widget.detail.type", "type", it->second.type),
.badge = tr("settings.entities.widget.kinds.named"),
@@ -349,7 +371,7 @@ namespace settings {
if (isBuiltInWidgetType(name)) {
continue;
}
addPickerEntry(entries, seen, name, name,
addPickerEntry(entries, seen, name, widgetInstanceDisplayLabel(name),
widget.type.empty() ? tr("settings.entities.widget.detail.custom")
: tr("settings.entities.widget.detail.type", "type", widget.type),
tr("settings.entities.widget.kinds.named"), WidgetReferenceKind::Named);
@@ -423,6 +445,10 @@ namespace settings {
{"always", "settings.widgets.options.always"},
{"on_hover", "settings.widgets.options.on-hover"},
};
const std::vector<WidgetSettingSelectOption> volumeDeviceOptions = {
{"output", "settings.widgets.options.output"},
{"input", "settings.widgets.options.input"},
};
const std::vector<WidgetSettingSelectOption> workspaceColorRoles = {
{"on_surface", ""}, {"primary", ""}, {"secondary", ""}, {"tertiary", ""}, {"error", ""},
};
@@ -494,6 +520,7 @@ namespace settings {
add(boolSpec("drawer", false));
add(intSpec("drawer_columns", 3, 1.0, 5.0, 1.0));
} else if (type == "volume") {
add(segmentedSpec("device", "output", volumeDeviceOptions));
add(boolSpec("show_label", true));
} else if (type == "wallpaper") {
add(stringSpec("glyph", "wallpaper-selector"));
+20 -4
View File
@@ -51,9 +51,24 @@ namespace {
namespace {
std::string normalizeFormatEscapes(std::string_view fmt) {
std::string out;
out.reserve(fmt.size());
for (std::size_t i = 0; i < fmt.size(); ++i) {
if (fmt[i] == '\\' && i + 1 < fmt.size() && fmt[i + 1] == 'n') {
out.push_back('\n');
++i;
} else {
out.push_back(fmt[i]);
}
}
return out;
}
bool shouldUseStrftimeCompat(std::string_view fmt) {
return fmt.find("%-") != std::string_view::npos ||
(fmt.find('{') == std::string_view::npos && fmt.find('%') != std::string_view::npos);
(fmt.find('%') != std::string_view::npos &&
(fmt.find('{') == std::string_view::npos || fmt.find("{:") != std::string_view::npos));
}
std::string strftimeSpec(std::string_view spec, const std::tm& local) {
@@ -129,19 +144,20 @@ namespace {
std::string formatLocalTime(const char* fmt) {
using namespace std::chrono;
const std::string normalizedFmt = normalizeFormatEscapes(fmt);
const auto now = floor<seconds>(system_clock::now());
const std::time_t raw = system_clock::to_time_t(now);
std::tm localTm{};
localtime_r(&raw, &localTm);
if (auto compat = formatStrftimeCompat(fmt, localTm)) {
if (auto compat = formatStrftimeCompat(normalizedFmt, localTm)) {
return *compat;
}
const auto local = current_zone()->to_local(now);
try {
return std::vformat(std::locale(""), fmt, std::make_format_args(local));
return std::vformat(std::locale(""), normalizedFmt, std::make_format_args(local));
} catch (...) {
return fmt;
return normalizedFmt;
}
}
+28
View File
@@ -0,0 +1,28 @@
#include "ui/controls/screen_corner.h"
#include "render/scene/screen_corner_node.h"
#include <memory>
ScreenCorner::ScreenCorner() {
auto corner = std::make_unique<ScreenCornerNode>();
m_corner = static_cast<ScreenCornerNode*>(addChild(std::move(corner)));
}
void ScreenCorner::setColor(const Color& color) { m_corner->setColor(color); }
void ScreenCorner::setCorner(ScreenCornerPosition position) { m_corner->setCorner(position); }
void ScreenCorner::setExponent(float exponent) { m_corner->setExponent(exponent); }
void ScreenCorner::setSoftness(float softness) { m_corner->setSoftness(softness); }
void ScreenCorner::setSize(float width, float height) {
Node::setSize(width, height);
m_corner->setFrameSize(width, height);
}
void ScreenCorner::setFrameSize(float width, float height) {
Node::setFrameSize(width, height);
m_corner->setFrameSize(width, height);
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include "render/core/color.h"
#include "render/core/render_styles.h"
#include "render/scene/node.h"
class ScreenCornerNode;
class ScreenCorner : public Node {
public:
ScreenCorner();
void setColor(const Color& color);
void setCorner(ScreenCornerPosition position);
void setExponent(float exponent);
void setSoftness(float softness);
void setSize(float width, float height) override;
void setFrameSize(float width, float height);
private:
ScreenCornerNode* m_corner = nullptr;
};
+2 -1
View File
@@ -515,7 +515,8 @@ bool WaylandConnection::sameWorkspaceModelSnapshot(const std::vector<WorkspaceMo
a.urgent == b.urgent && a.occupied == b.occupied;
};
auto sameAssignment = [](const WorkspaceWindowAssignment& a, const WorkspaceWindowAssignment& b) {
return a.windowId == b.windowId && a.workspaceKey == b.workspaceKey && a.appId == b.appId;
return a.windowId == b.windowId && a.workspaceKey == b.workspaceKey && a.appId == b.appId && a.title == b.title &&
a.x == b.x && a.y == b.y;
};
if (lhs.size() != rhs.size()) {