mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge branch 'noctalia-dev:v5' into v5
This commit is contained in:
@@ -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"
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -17,6 +17,7 @@ enum class NodeType : std::uint8_t {
|
||||
Image,
|
||||
Glyph,
|
||||
Spinner,
|
||||
ScreenCorner,
|
||||
AudioSpectrum,
|
||||
Effect,
|
||||
Graph,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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_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(m_label->width(), m_label->height());
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,10 +737,14 @@ 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);
|
||||
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);
|
||||
@@ -746,8 +758,16 @@ void NotificationToast::addCardToInstance(Instance& inst, std::size_t entryIndex
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user