mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(renderer): complete Phase 1 — font fallback, images, animations, multi-monitor
This commit is contained in:
@@ -140,12 +140,17 @@ add_executable(noctalia
|
||||
src/core/Log.cpp
|
||||
src/font/FontService.cpp
|
||||
src/render/scene/Node.cpp
|
||||
src/render/Animation.cpp
|
||||
src/render/AnimationManager.cpp
|
||||
src/render/GlRenderer.cpp
|
||||
src/render/ImageLoaders.cpp
|
||||
src/render/ImageProgram.cpp
|
||||
src/render/LinearGradientProgram.cpp
|
||||
src/render/RoundedRectProgram.cpp
|
||||
src/render/ShaderProgram.cpp
|
||||
src/render/MsdfTextProgram.cpp
|
||||
src/render/MsdfTextRenderer.cpp
|
||||
src/render/TextureManager.cpp
|
||||
src/shell/Bar.cpp
|
||||
src/wayland/Surface.cpp
|
||||
src/wayland/LayerSurface.cpp
|
||||
@@ -160,6 +165,8 @@ add_dependencies(noctalia noctalia_wayland_protocols)
|
||||
|
||||
target_include_directories(noctalia PRIVATE
|
||||
src
|
||||
third_party/stb
|
||||
third_party/nanosvg
|
||||
"${GENERATED_PROTOCOL_DIR}"
|
||||
${WAYLAND_CLIENT_INCLUDE_DIRS}
|
||||
${EGL_INCLUDE_DIRS}
|
||||
|
||||
@@ -94,11 +94,11 @@ third_party/
|
||||
|
||||
Everything else depends on these.
|
||||
|
||||
- [X] Retained scene graph (`Node`, `RectNode`, `TextNode`, `ImageNode`)
|
||||
- [ ] Per-surface invalidation and property animations
|
||||
- [ ] Image/texture loading
|
||||
- [ ] Multi-monitor bar instances (one per output, hot-plug)
|
||||
- [ ] Font fallback, DPI-aware metrics, text truncation
|
||||
- [x] Retained scene graph (`Node`, `RectNode`, `TextNode`, `ImageNode`)
|
||||
- [x] Font fallback chain (fontconfig `FcFontSort`), DPI-aware buffer scaling, text truncation with ellipsis
|
||||
- [x] Image/texture loading (stb_image + nanosvg, ARGB pixmap support, `TextureManager`)
|
||||
- [x] Property animation system (easing functions, frame-callback driven `AnimationManager`)
|
||||
- [x] Multi-monitor bar instances (one `LayerSurface` per output, hot-plug add/remove)
|
||||
|
||||
### Phase 2 -- Minimum viable bar
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "font/FontService.hpp"
|
||||
|
||||
#include "core/Log.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
FontService::FontService() {
|
||||
@@ -43,3 +45,61 @@ std::string FontService::resolvePath(const std::string& family) const {
|
||||
FcPatternDestroy(match);
|
||||
return path;
|
||||
}
|
||||
|
||||
std::vector<ResolvedFont> FontService::resolveFallbackChain(const std::string& family, int limit) const {
|
||||
FcPattern* pattern = FcNameParse(reinterpret_cast<const FcChar8*>(family.c_str()));
|
||||
if (pattern == nullptr) {
|
||||
throw std::runtime_error("FcNameParse failed for: " + family);
|
||||
}
|
||||
|
||||
FcConfigSubstitute(m_config, pattern, FcMatchPattern);
|
||||
FcDefaultSubstitute(pattern);
|
||||
|
||||
FcResult result = FcResultNoMatch;
|
||||
FcFontSet* fontSet = FcFontSort(m_config, pattern, FcTrue, nullptr, &result);
|
||||
FcPatternDestroy(pattern);
|
||||
|
||||
if (fontSet == nullptr || result != FcResultMatch) {
|
||||
throw std::runtime_error("no fonts found for: " + family);
|
||||
}
|
||||
|
||||
std::vector<ResolvedFont> chain;
|
||||
for (int i = 0; i < fontSet->nfont && static_cast<int>(chain.size()) < limit; ++i) {
|
||||
FcChar8* filePath = nullptr;
|
||||
int faceIndex = 0;
|
||||
if (FcPatternGetString(fontSet->fonts[i], FC_FILE, 0, &filePath) != FcResultMatch ||
|
||||
filePath == nullptr) {
|
||||
continue;
|
||||
}
|
||||
FcPatternGetInteger(fontSet->fonts[i], FC_INDEX, 0, &faceIndex);
|
||||
|
||||
std::string path(reinterpret_cast<const char*>(filePath));
|
||||
|
||||
// Skip duplicates (same file + face index)
|
||||
bool duplicate = false;
|
||||
for (const auto& existing : chain) {
|
||||
if (existing.path == path && existing.faceIndex == faceIndex) {
|
||||
duplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (duplicate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
chain.push_back(ResolvedFont{.path = std::move(path), .faceIndex = faceIndex});
|
||||
}
|
||||
|
||||
FcFontSetDestroy(fontSet);
|
||||
|
||||
if (chain.empty()) {
|
||||
throw std::runtime_error("no fonts resolved for: " + family);
|
||||
}
|
||||
|
||||
logDebug("font fallback chain for \"{}\" ({} fonts):", family, chain.size());
|
||||
for (const auto& font : chain) {
|
||||
logDebug(" {} [{}]", font.path, font.faceIndex);
|
||||
}
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
#include <fontconfig/fontconfig.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct ResolvedFont {
|
||||
std::string path;
|
||||
int faceIndex = 0;
|
||||
};
|
||||
|
||||
class FontService {
|
||||
public:
|
||||
@@ -13,6 +19,7 @@ public:
|
||||
FontService& operator=(const FontService&) = delete;
|
||||
|
||||
[[nodiscard]] std::string resolvePath(const std::string& family) const;
|
||||
[[nodiscard]] std::vector<ResolvedFont> resolveFallbackChain(const std::string& family, int limit = 8) const;
|
||||
|
||||
private:
|
||||
FcConfig* m_config = nullptr;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "render/Animation.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
float applyEasing(Easing easing, float t) {
|
||||
t = std::clamp(t, 0.0f, 1.0f);
|
||||
|
||||
switch (easing) {
|
||||
case Easing::Linear:
|
||||
return t;
|
||||
|
||||
case Easing::EaseInQuad:
|
||||
return t * t;
|
||||
|
||||
case Easing::EaseOutQuad:
|
||||
return t * (2.0f - t);
|
||||
|
||||
case Easing::EaseInOutQuad:
|
||||
if (t < 0.5f) {
|
||||
return 2.0f * t * t;
|
||||
}
|
||||
return -1.0f + (4.0f - 2.0f * t) * t;
|
||||
|
||||
case Easing::EaseOutCubic: {
|
||||
const float f = t - 1.0f;
|
||||
return f * f * f + 1.0f;
|
||||
}
|
||||
|
||||
case Easing::EaseInOutCubic:
|
||||
if (t < 0.5f) {
|
||||
return 4.0f * t * t * t;
|
||||
} else {
|
||||
const float f = 2.0f * t - 2.0f;
|
||||
return 0.5f * f * f * f + 1.0f;
|
||||
}
|
||||
|
||||
case Easing::EaseOutBack: {
|
||||
constexpr float c1 = 1.70158f;
|
||||
constexpr float c3 = c1 + 1.0f;
|
||||
const float f = t - 1.0f;
|
||||
return 1.0f + c3 * f * f * f + c1 * f * f;
|
||||
}
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
enum class Easing : std::uint8_t {
|
||||
Linear,
|
||||
EaseInQuad,
|
||||
EaseOutQuad,
|
||||
EaseInOutQuad,
|
||||
EaseOutCubic,
|
||||
EaseInOutCubic,
|
||||
EaseOutBack,
|
||||
};
|
||||
|
||||
float applyEasing(Easing easing, float t);
|
||||
|
||||
struct Animation {
|
||||
float startValue = 0.0f;
|
||||
float endValue = 0.0f;
|
||||
float durationMs = 0.0f;
|
||||
float elapsedMs = 0.0f;
|
||||
Easing easing = Easing::EaseOutQuad;
|
||||
std::function<void(float)> setter;
|
||||
std::function<void()> onComplete;
|
||||
bool finished = false;
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "render/AnimationManager.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
AnimationManager::Id AnimationManager::animate(float from, float to, float durationMs,
|
||||
Easing easing,
|
||||
std::function<void(float)> setter,
|
||||
std::function<void()> onComplete) {
|
||||
Id id = m_nextId++;
|
||||
m_animations.push_back(Entry{
|
||||
.id = id,
|
||||
.animation = Animation{
|
||||
.startValue = from,
|
||||
.endValue = to,
|
||||
.durationMs = durationMs,
|
||||
.easing = easing,
|
||||
.setter = std::move(setter),
|
||||
.onComplete = std::move(onComplete),
|
||||
},
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
void AnimationManager::cancel(Id id) {
|
||||
std::erase_if(m_animations, [id](const Entry& e) { return e.id == id; });
|
||||
}
|
||||
|
||||
void AnimationManager::tick(float deltaMs) {
|
||||
for (auto& entry : m_animations) {
|
||||
auto& anim = entry.animation;
|
||||
if (anim.finished) {
|
||||
continue;
|
||||
}
|
||||
|
||||
anim.elapsedMs += deltaMs;
|
||||
float t = anim.durationMs > 0.0f ? anim.elapsedMs / anim.durationMs : 1.0f;
|
||||
|
||||
if (t >= 1.0f) {
|
||||
t = 1.0f;
|
||||
anim.finished = true;
|
||||
}
|
||||
|
||||
float easedT = applyEasing(anim.easing, t);
|
||||
float value = anim.startValue + (anim.endValue - anim.startValue) * easedT;
|
||||
|
||||
if (anim.setter) {
|
||||
anim.setter(value);
|
||||
}
|
||||
|
||||
if (anim.finished && anim.onComplete) {
|
||||
anim.onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
std::erase_if(m_animations, [](const Entry& e) { return e.animation.finished; });
|
||||
}
|
||||
|
||||
bool AnimationManager::hasActive() const {
|
||||
return !m_animations.empty();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "render/Animation.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
class AnimationManager {
|
||||
public:
|
||||
using Id = std::uint32_t;
|
||||
|
||||
AnimationManager() = default;
|
||||
|
||||
Id animate(float from, float to, float durationMs, Easing easing,
|
||||
std::function<void(float)> setter,
|
||||
std::function<void()> onComplete = {});
|
||||
void cancel(Id id);
|
||||
void tick(float deltaMs);
|
||||
[[nodiscard]] bool hasActive() const;
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
Id id = 0;
|
||||
Animation animation;
|
||||
};
|
||||
|
||||
std::vector<Entry> m_animations;
|
||||
Id m_nextId = 1;
|
||||
};
|
||||
+38
-18
@@ -75,8 +75,9 @@ void GlRenderer::bind(wl_display* display, wl_surface* surface) {
|
||||
}
|
||||
}
|
||||
|
||||
void GlRenderer::resize(std::uint32_t width, std::uint32_t height) {
|
||||
if (width == 0 || height == 0) {
|
||||
void GlRenderer::resize(std::uint32_t bufferWidth, std::uint32_t bufferHeight,
|
||||
std::uint32_t logicalWidth, std::uint32_t logicalHeight) {
|
||||
if (bufferWidth == 0 || bufferHeight == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,13 +88,13 @@ void GlRenderer::resize(std::uint32_t width, std::uint32_t height) {
|
||||
if (m_window == nullptr) {
|
||||
m_window = wl_egl_window_create(
|
||||
m_surface,
|
||||
static_cast<int>(width),
|
||||
static_cast<int>(height));
|
||||
static_cast<int>(bufferWidth),
|
||||
static_cast<int>(bufferHeight));
|
||||
if (m_window == nullptr) {
|
||||
throw std::runtime_error("wl_egl_window_create failed");
|
||||
}
|
||||
} else {
|
||||
wl_egl_window_resize(m_window, static_cast<int>(width), static_cast<int>(height), 0, 0);
|
||||
wl_egl_window_resize(m_window, static_cast<int>(bufferWidth), static_cast<int>(bufferHeight), 0, 0);
|
||||
}
|
||||
|
||||
if (m_eglSurface == EGL_NO_SURFACE) {
|
||||
@@ -111,15 +112,18 @@ void GlRenderer::resize(std::uint32_t width, std::uint32_t height) {
|
||||
throw std::runtime_error("eglMakeCurrent failed during resize");
|
||||
}
|
||||
|
||||
m_surfaceWidth = width;
|
||||
m_surfaceHeight = height;
|
||||
m_bufferWidth = bufferWidth;
|
||||
m_bufferHeight = bufferHeight;
|
||||
m_logicalWidth = logicalWidth;
|
||||
m_logicalHeight = logicalHeight;
|
||||
m_imageProgram.ensureInitialized();
|
||||
m_linearGradientProgram.ensureInitialized();
|
||||
m_roundedRectProgram.ensureInitialized();
|
||||
const auto fontPath = m_fontService.resolvePath("sans-serif");
|
||||
m_textRenderer.initialize(fontPath.c_str());
|
||||
const auto fonts = m_fontService.resolveFallbackChain("sans-serif");
|
||||
m_textRenderer.initialize(fonts);
|
||||
}
|
||||
|
||||
void GlRenderer::render(std::uint32_t width, std::uint32_t height) {
|
||||
void GlRenderer::render() {
|
||||
if (m_eglDisplay == EGL_NO_DISPLAY || m_eglSurface == EGL_NO_SURFACE || m_eglContext == EGL_NO_CONTEXT) {
|
||||
throw std::runtime_error("OpenGL renderer is not ready");
|
||||
}
|
||||
@@ -128,7 +132,7 @@ void GlRenderer::render(std::uint32_t width, std::uint32_t height) {
|
||||
throw std::runtime_error("eglMakeCurrent failed");
|
||||
}
|
||||
|
||||
glViewport(0, 0, static_cast<GLint>(width), static_cast<GLint>(height));
|
||||
glViewport(0, 0, static_cast<GLint>(m_bufferWidth), static_cast<GLint>(m_bufferHeight));
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
@@ -162,8 +166,8 @@ void GlRenderer::renderNode(const Node* node, float parentX, float parentY, floa
|
||||
const float absY = parentY + node->y();
|
||||
const float effectiveOpacity = parentOpacity * node->opacity();
|
||||
|
||||
const auto sw = static_cast<float>(m_surfaceWidth);
|
||||
const auto sh = static_cast<float>(m_surfaceHeight);
|
||||
const auto sw = static_cast<float>(m_logicalWidth);
|
||||
const auto sh = static_cast<float>(m_logicalHeight);
|
||||
|
||||
switch (node->type()) {
|
||||
case NodeType::Rect: {
|
||||
@@ -180,13 +184,25 @@ void GlRenderer::renderNode(const Node* node, float parentX, float parentY, floa
|
||||
if (!text->text().empty()) {
|
||||
auto color = text->color();
|
||||
color.a *= effectiveOpacity;
|
||||
m_textRenderer.draw(sw, sh, absX, absY, text->text(), text->fontSize(), color);
|
||||
if (text->maxWidth() > 0.0f) {
|
||||
auto truncated = m_textRenderer.truncate(text->text(), text->fontSize(), text->maxWidth());
|
||||
m_textRenderer.draw(sw, sh, absX, absY, truncated.text, text->fontSize(), color);
|
||||
} else {
|
||||
m_textRenderer.draw(sw, sh, absX, absY, text->text(), text->fontSize(), color);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NodeType::Image:
|
||||
// Placeholder -- image loading not yet implemented
|
||||
case NodeType::Image: {
|
||||
const auto* img = static_cast<const ImageNode*>(node);
|
||||
if (img->textureId() != 0) {
|
||||
auto tint = img->tint();
|
||||
tint.a *= effectiveOpacity;
|
||||
m_imageProgram.draw(img->textureId(), sw, sh, absX, absY,
|
||||
node->width(), node->height(), tint, effectiveOpacity);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NodeType::Base:
|
||||
break;
|
||||
}
|
||||
@@ -197,11 +213,15 @@ void GlRenderer::renderNode(const Node* node, float parentX, float parentY, floa
|
||||
}
|
||||
|
||||
void GlRenderer::cleanup() {
|
||||
m_textureManager.cleanup();
|
||||
m_imageProgram.destroy();
|
||||
m_linearGradientProgram.destroy();
|
||||
m_roundedRectProgram.destroy();
|
||||
m_textRenderer.cleanup();
|
||||
m_surfaceWidth = 0;
|
||||
m_surfaceHeight = 0;
|
||||
m_bufferWidth = 0;
|
||||
m_bufferHeight = 0;
|
||||
m_logicalWidth = 0;
|
||||
m_logicalHeight = 0;
|
||||
|
||||
if (m_eglDisplay != EGL_NO_DISPLAY) {
|
||||
eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "font/FontService.hpp"
|
||||
#include "render/ImageProgram.hpp"
|
||||
#include "render/LinearGradientProgram.hpp"
|
||||
#include "render/RoundedRectProgram.hpp"
|
||||
#include "render/Renderer.hpp"
|
||||
#include "render/MsdfTextRenderer.hpp"
|
||||
#include "render/TextureManager.hpp"
|
||||
|
||||
#include <EGL/egl.h>
|
||||
|
||||
@@ -19,8 +21,9 @@ public:
|
||||
[[nodiscard]] const char* name() const noexcept override;
|
||||
|
||||
void bind(wl_display* display, wl_surface* surface) override;
|
||||
void resize(std::uint32_t width, std::uint32_t height) override;
|
||||
void render(std::uint32_t width, std::uint32_t height) override;
|
||||
void resize(std::uint32_t bufferWidth, std::uint32_t bufferHeight,
|
||||
std::uint32_t logicalWidth, std::uint32_t logicalHeight) override;
|
||||
void render() override;
|
||||
void setScene(Node* root) override;
|
||||
[[nodiscard]] TextMetrics measureText(std::string_view text, float fontSize) override;
|
||||
|
||||
@@ -36,10 +39,14 @@ private:
|
||||
EGLContext m_eglContext = EGL_NO_CONTEXT;
|
||||
EGLSurface m_eglSurface = EGL_NO_SURFACE;
|
||||
FontService m_fontService;
|
||||
ImageProgram m_imageProgram;
|
||||
LinearGradientProgram m_linearGradientProgram;
|
||||
RoundedRectProgram m_roundedRectProgram;
|
||||
MsdfTextRenderer m_textRenderer;
|
||||
TextureManager m_textureManager;
|
||||
Node* m_sceneRoot = nullptr;
|
||||
std::uint32_t m_surfaceWidth = 0;
|
||||
std::uint32_t m_surfaceHeight = 0;
|
||||
std::uint32_t m_bufferWidth = 0;
|
||||
std::uint32_t m_bufferHeight = 0;
|
||||
std::uint32_t m_logicalWidth = 0;
|
||||
std::uint32_t m_logicalHeight = 0;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wconversion"
|
||||
#pragma GCC diagnostic ignored "-Wsign-conversion"
|
||||
#pragma GCC diagnostic ignored "-Wfloat-conversion"
|
||||
#pragma GCC diagnostic ignored "-Wold-style-cast"
|
||||
#pragma GCC diagnostic ignored "-Wshadow"
|
||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STBI_ONLY_PNG
|
||||
#define STBI_ONLY_JPEG
|
||||
#define STBI_NO_STDIO
|
||||
#include <stb_image.h>
|
||||
|
||||
#define NANOSVG_IMPLEMENTATION
|
||||
#include <nanosvg.h>
|
||||
|
||||
#define NANOSVGRAST_IMPLEMENTATION
|
||||
#include <nanosvgrast.h>
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
@@ -0,0 +1,129 @@
|
||||
#include "render/ImageProgram.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kVertexShaderSource[] = R"(
|
||||
precision highp float;
|
||||
|
||||
attribute vec2 a_position;
|
||||
attribute vec2 a_texcoord;
|
||||
uniform vec2 u_surface_size;
|
||||
uniform vec4 u_rect;
|
||||
varying vec2 v_texcoord;
|
||||
|
||||
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 pixel_pos = u_rect.xy + (a_position * u_rect.zw);
|
||||
v_texcoord = a_texcoord;
|
||||
gl_Position = vec4(to_ndc(pixel_pos), 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr char kFragmentShaderSource[] = R"(
|
||||
precision mediump float;
|
||||
|
||||
uniform sampler2D u_texture;
|
||||
uniform vec4 u_tint;
|
||||
uniform float u_opacity;
|
||||
varying vec2 v_texcoord;
|
||||
|
||||
void main() {
|
||||
vec4 texel = texture2D(u_texture, v_texcoord);
|
||||
gl_FragColor = texel * u_tint * vec4(1.0, 1.0, 1.0, u_opacity);
|
||||
}
|
||||
)";
|
||||
|
||||
} // namespace
|
||||
|
||||
void ImageProgram::ensureInitialized() {
|
||||
if (m_program.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_program.create(kVertexShaderSource, kFragmentShaderSource);
|
||||
m_positionLocation = glGetAttribLocation(m_program.id(), "a_position");
|
||||
m_texCoordLocation = glGetAttribLocation(m_program.id(), "a_texcoord");
|
||||
m_surfaceSizeLocation = glGetUniformLocation(m_program.id(), "u_surface_size");
|
||||
m_rectLocation = glGetUniformLocation(m_program.id(), "u_rect");
|
||||
m_tintLocation = glGetUniformLocation(m_program.id(), "u_tint");
|
||||
m_opacityLocation = glGetUniformLocation(m_program.id(), "u_opacity");
|
||||
m_samplerLocation = glGetUniformLocation(m_program.id(), "u_texture");
|
||||
|
||||
if (m_positionLocation < 0 ||
|
||||
m_texCoordLocation < 0 ||
|
||||
m_surfaceSizeLocation < 0 ||
|
||||
m_rectLocation < 0 ||
|
||||
m_tintLocation < 0 ||
|
||||
m_opacityLocation < 0 ||
|
||||
m_samplerLocation < 0) {
|
||||
throw std::runtime_error("failed to query image shader locations");
|
||||
}
|
||||
}
|
||||
|
||||
void ImageProgram::destroy() {
|
||||
m_program.destroy();
|
||||
m_positionLocation = -1;
|
||||
m_texCoordLocation = -1;
|
||||
m_surfaceSizeLocation = -1;
|
||||
m_rectLocation = -1;
|
||||
m_tintLocation = -1;
|
||||
m_opacityLocation = -1;
|
||||
m_samplerLocation = -1;
|
||||
}
|
||||
|
||||
void ImageProgram::draw(GLuint texture,
|
||||
float surfaceWidth,
|
||||
float surfaceHeight,
|
||||
float x,
|
||||
float y,
|
||||
float width,
|
||||
float height,
|
||||
const Color& tint,
|
||||
float opacity) const {
|
||||
if (!m_program.isValid() || texture == 0 || width <= 0.0f || height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::array<GLfloat, 12> positions = {
|
||||
0.0f, 0.0f,
|
||||
1.0f, 0.0f,
|
||||
0.0f, 1.0f,
|
||||
0.0f, 1.0f,
|
||||
1.0f, 0.0f,
|
||||
1.0f, 1.0f,
|
||||
};
|
||||
|
||||
const std::array<GLfloat, 12> texcoords = {
|
||||
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);
|
||||
glUniform4f(m_rectLocation, x, y, width, height);
|
||||
glUniform4f(m_tintLocation, tint.r, tint.g, tint.b, tint.a);
|
||||
glUniform1f(m_opacityLocation, opacity);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glUniform1i(m_samplerLocation, 0);
|
||||
const auto posAttr = static_cast<GLuint>(m_positionLocation);
|
||||
const auto texAttr = static_cast<GLuint>(m_texCoordLocation);
|
||||
glVertexAttribPointer(posAttr, 2, GL_FLOAT, GL_FALSE, 0, positions.data());
|
||||
glVertexAttribPointer(texAttr, 2, GL_FLOAT, GL_FALSE, 0, texcoords.data());
|
||||
glEnableVertexAttribArray(posAttr);
|
||||
glEnableVertexAttribArray(texAttr);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
glDisableVertexAttribArray(posAttr);
|
||||
glDisableVertexAttribArray(texAttr);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "render/Color.hpp"
|
||||
#include "render/ShaderProgram.hpp"
|
||||
|
||||
#include <GLES2/gl2.h>
|
||||
|
||||
class ImageProgram {
|
||||
public:
|
||||
ImageProgram() = default;
|
||||
~ImageProgram() = default;
|
||||
|
||||
ImageProgram(const ImageProgram&) = delete;
|
||||
ImageProgram& operator=(const ImageProgram&) = delete;
|
||||
|
||||
void ensureInitialized();
|
||||
void destroy();
|
||||
|
||||
void draw(GLuint texture,
|
||||
float surfaceWidth,
|
||||
float surfaceHeight,
|
||||
float x,
|
||||
float y,
|
||||
float width,
|
||||
float height,
|
||||
const Color& tint,
|
||||
float opacity) const;
|
||||
|
||||
private:
|
||||
ShaderProgram m_program;
|
||||
GLint m_positionLocation = -1;
|
||||
GLint m_texCoordLocation = -1;
|
||||
GLint m_surfaceSizeLocation = -1;
|
||||
GLint m_rectLocation = -1;
|
||||
GLint m_tintLocation = -1;
|
||||
GLint m_opacityLocation = -1;
|
||||
GLint m_samplerLocation = -1;
|
||||
};
|
||||
+287
-119
@@ -1,5 +1,8 @@
|
||||
#include "render/MsdfTextRenderer.hpp"
|
||||
|
||||
#include "core/Log.hpp"
|
||||
#include "font/FontService.hpp"
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wshadow"
|
||||
#include <msdfgen.h>
|
||||
@@ -26,36 +29,58 @@ MsdfTextRenderer::~MsdfTextRenderer() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
void MsdfTextRenderer::initialize(const char* fontPath) {
|
||||
if (m_face != nullptr) {
|
||||
void MsdfTextRenderer::initialize(const std::vector<ResolvedFont>& fonts) {
|
||||
if (!m_fontSlots.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fonts.empty()) {
|
||||
throw std::runtime_error("no fonts provided to MsdfTextRenderer");
|
||||
}
|
||||
|
||||
if (FT_Init_FreeType(&m_library) != 0) {
|
||||
throw std::runtime_error("FT_Init_FreeType failed");
|
||||
}
|
||||
|
||||
if (FT_New_Face(m_library, fontPath, 0, &m_face) != 0) {
|
||||
throw std::runtime_error(std::string("FT_New_Face failed for: ") + fontPath);
|
||||
for (const auto& font : fonts) {
|
||||
FontSlot slot;
|
||||
if (FT_New_Face(m_library, font.path.c_str(), font.faceIndex, &slot.face) != 0) {
|
||||
logWarn("failed to load fallback font: {}", font.path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (FT_Set_Pixel_Sizes(slot.face, 0, static_cast<FT_UInt>(kAtlasEmSize)) != 0) {
|
||||
FT_Done_Face(slot.face);
|
||||
logWarn("failed to set pixel size for: {}", font.path);
|
||||
continue;
|
||||
}
|
||||
|
||||
slot.hbFont = hb_ft_font_create_referenced(slot.face);
|
||||
if (slot.hbFont == nullptr) {
|
||||
FT_Done_Face(slot.face);
|
||||
logWarn("hb_ft_font_create_referenced failed for: {}", font.path);
|
||||
continue;
|
||||
}
|
||||
|
||||
slot.fontHandle = msdfgen::adoptFreetypeFont(slot.face);
|
||||
if (slot.fontHandle == nullptr) {
|
||||
hb_font_destroy(slot.hbFont);
|
||||
FT_Done_Face(slot.face);
|
||||
logWarn("msdfgen::adoptFreetypeFont failed for: {}", font.path);
|
||||
continue;
|
||||
}
|
||||
|
||||
m_fontSlots.push_back(slot);
|
||||
}
|
||||
|
||||
if (FT_Set_Pixel_Sizes(m_face, 0, static_cast<FT_UInt>(kAtlasEmSize)) != 0) {
|
||||
throw std::runtime_error("FT_Set_Pixel_Sizes failed");
|
||||
if (m_fontSlots.empty()) {
|
||||
throw std::runtime_error("no fonts could be loaded");
|
||||
}
|
||||
|
||||
m_hbFont = hb_ft_font_create_referenced(m_face);
|
||||
if (m_hbFont == nullptr) {
|
||||
throw std::runtime_error("hb_ft_font_create_referenced failed");
|
||||
}
|
||||
m_currentShapingSize = kAtlasEmSize;
|
||||
|
||||
m_fontHandle = msdfgen::adoptFreetypeFont(m_face);
|
||||
if (m_fontHandle == nullptr) {
|
||||
throw std::runtime_error("msdfgen::adoptFreetypeFont failed");
|
||||
}
|
||||
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
ensureAtlasInitialized();
|
||||
ensureAtlasPage(0);
|
||||
m_program.ensureInitialized();
|
||||
}
|
||||
|
||||
@@ -64,20 +89,7 @@ MsdfTextRenderer::TextMetrics MsdfTextRenderer::measure(std::string_view text, f
|
||||
return {};
|
||||
}
|
||||
|
||||
setShapingSize(fontSize);
|
||||
|
||||
hb_buffer_t* buffer = hb_buffer_create();
|
||||
if (buffer == nullptr) {
|
||||
throw std::runtime_error("hb_buffer_create failed");
|
||||
}
|
||||
|
||||
hb_buffer_add_utf8(buffer, text.data(), static_cast<int>(text.size()), 0, static_cast<int>(text.size()));
|
||||
hb_buffer_guess_segment_properties(buffer);
|
||||
hb_shape(m_hbFont, buffer, nullptr, 0);
|
||||
|
||||
unsigned int glyphCount = 0;
|
||||
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(buffer, &glyphCount);
|
||||
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(buffer, &glyphCount);
|
||||
auto shaped = shapeWithFallback(text, fontSize);
|
||||
|
||||
const float scale = fontSize / kAtlasEmSize;
|
||||
float width = 0.0f;
|
||||
@@ -86,11 +98,10 @@ MsdfTextRenderer::TextMetrics MsdfTextRenderer::measure(std::string_view text, f
|
||||
float maxBottom = 0.0f;
|
||||
bool hasBounds = false;
|
||||
|
||||
for (unsigned int i = 0; i < glyphCount; ++i) {
|
||||
const auto glyphIndex = glyphInfos[i].codepoint;
|
||||
Glyph& glyph = loadGlyph(glyphIndex);
|
||||
const float xOffset = static_cast<float>(glyphPositions[i].x_offset) / 64.0f;
|
||||
const float yOffset = static_cast<float>(glyphPositions[i].y_offset) / 64.0f;
|
||||
for (const auto& sg : shaped) {
|
||||
Glyph& glyph = loadGlyph(sg.slotIndex, sg.glyphIndex);
|
||||
const float xOffset = static_cast<float>(sg.position.x_offset) / 64.0f;
|
||||
const float yOffset = static_cast<float>(sg.position.y_offset) / 64.0f;
|
||||
const float glyphLeft = penX + xOffset + glyph.bearingX * scale;
|
||||
const float glyphTop = -yOffset - glyph.bearingY * scale;
|
||||
const float glyphBottom = glyphTop + glyph.atlasHeight * scale;
|
||||
@@ -109,13 +120,11 @@ MsdfTextRenderer::TextMetrics MsdfTextRenderer::measure(std::string_view text, f
|
||||
}
|
||||
}
|
||||
|
||||
penX += static_cast<float>(glyphPositions[i].x_advance) / 64.0f;
|
||||
penX += static_cast<float>(sg.position.x_advance) / 64.0f;
|
||||
}
|
||||
|
||||
width = std::max(width, penX);
|
||||
|
||||
hb_buffer_destroy(buffer);
|
||||
|
||||
return TextMetrics{
|
||||
.width = width,
|
||||
.top = minTop,
|
||||
@@ -123,6 +132,48 @@ MsdfTextRenderer::TextMetrics MsdfTextRenderer::measure(std::string_view text, f
|
||||
};
|
||||
}
|
||||
|
||||
MsdfTextRenderer::TruncatedText MsdfTextRenderer::truncate(
|
||||
std::string_view text, float fontSize, float maxWidth) {
|
||||
|
||||
if (text.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto fullMetrics = measure(text, fontSize);
|
||||
if (fullMetrics.width <= maxWidth) {
|
||||
return TruncatedText{.text = std::string(text), .width = fullMetrics.width};
|
||||
}
|
||||
|
||||
static constexpr std::string_view kEllipsis = "\xe2\x80\xa6"; // U+2026 "…"
|
||||
auto ellipsisMetrics = measure(kEllipsis, fontSize);
|
||||
const float availableWidth = maxWidth - ellipsisMetrics.width;
|
||||
|
||||
if (availableWidth <= 0.0f) {
|
||||
return TruncatedText{.text = std::string(kEllipsis), .width = ellipsisMetrics.width};
|
||||
}
|
||||
|
||||
// Find the longest prefix that fits. Scan backwards from end on UTF-8 boundaries.
|
||||
std::size_t cutPos = text.size();
|
||||
while (cutPos > 0) {
|
||||
--cutPos;
|
||||
// Skip continuation bytes (10xxxxxx)
|
||||
if ((static_cast<unsigned char>(text[cutPos]) & 0xC0) == 0x80) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string_view prefix = text.substr(0, cutPos);
|
||||
auto prefixMetrics = measure(prefix, fontSize);
|
||||
if (prefixMetrics.width <= availableWidth) {
|
||||
std::string result(prefix);
|
||||
result.append(kEllipsis);
|
||||
auto resultMetrics = measure(result, fontSize);
|
||||
return TruncatedText{.text = std::move(result), .width = resultMetrics.width};
|
||||
}
|
||||
}
|
||||
|
||||
return TruncatedText{.text = std::string(kEllipsis), .width = ellipsisMetrics.width};
|
||||
}
|
||||
|
||||
void MsdfTextRenderer::draw(float surfaceWidth,
|
||||
float surfaceHeight,
|
||||
float x,
|
||||
@@ -134,40 +185,27 @@ void MsdfTextRenderer::draw(float surfaceWidth,
|
||||
return;
|
||||
}
|
||||
|
||||
setShapingSize(fontSize);
|
||||
|
||||
hb_buffer_t* buffer = hb_buffer_create();
|
||||
if (buffer == nullptr) {
|
||||
throw std::runtime_error("hb_buffer_create failed");
|
||||
}
|
||||
|
||||
hb_buffer_add_utf8(buffer, text.data(), static_cast<int>(text.size()), 0, static_cast<int>(text.size()));
|
||||
hb_buffer_guess_segment_properties(buffer);
|
||||
hb_shape(m_hbFont, buffer, nullptr, 0);
|
||||
|
||||
unsigned int glyphCount = 0;
|
||||
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(buffer, &glyphCount);
|
||||
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(buffer, &glyphCount);
|
||||
auto shaped = shapeWithFallback(text, fontSize);
|
||||
|
||||
const float scale = fontSize / kAtlasEmSize;
|
||||
const float pxRange = std::max(static_cast<float>(kDistanceRange) * scale, 1.0f);
|
||||
float penX = x;
|
||||
float penY = std::round(baselineY);
|
||||
|
||||
for (unsigned int i = 0; i < glyphCount; ++i) {
|
||||
const auto glyphIndex = glyphInfos[i].codepoint;
|
||||
Glyph& glyph = loadGlyph(glyphIndex);
|
||||
for (const auto& sg : shaped) {
|
||||
Glyph& glyph = loadGlyph(sg.slotIndex, sg.glyphIndex);
|
||||
|
||||
if (glyph.atlasWidth > 0.0f && glyph.atlasHeight > 0.0f) {
|
||||
const float xOffset = static_cast<float>(glyphPositions[i].x_offset) / 64.0f;
|
||||
const float yOffset = static_cast<float>(glyphPositions[i].y_offset) / 64.0f;
|
||||
const float xOffset = static_cast<float>(sg.position.x_offset) / 64.0f;
|
||||
const float yOffset = static_cast<float>(sg.position.y_offset) / 64.0f;
|
||||
const float glyphX = penX + xOffset + glyph.bearingX * scale;
|
||||
const float glyphY = penY - yOffset - glyph.bearingY * scale;
|
||||
const float glyphW = glyph.atlasWidth * scale;
|
||||
const float glyphH = glyph.atlasHeight * scale;
|
||||
|
||||
GLuint atlasTexture = m_atlasPages[glyph.atlasPage];
|
||||
m_program.draw(
|
||||
m_atlasTexture,
|
||||
atlasTexture,
|
||||
surfaceWidth,
|
||||
surfaceHeight,
|
||||
glyphX,
|
||||
@@ -182,18 +220,18 @@ void MsdfTextRenderer::draw(float surfaceWidth,
|
||||
color);
|
||||
}
|
||||
|
||||
penX += static_cast<float>(glyphPositions[i].x_advance) / 64.0f;
|
||||
penY -= static_cast<float>(glyphPositions[i].y_advance) / 64.0f;
|
||||
penX += static_cast<float>(sg.position.x_advance) / 64.0f;
|
||||
penY -= static_cast<float>(sg.position.y_advance) / 64.0f;
|
||||
}
|
||||
|
||||
hb_buffer_destroy(buffer);
|
||||
}
|
||||
|
||||
void MsdfTextRenderer::cleanup() {
|
||||
if (m_atlasTexture != 0) {
|
||||
glDeleteTextures(1, &m_atlasTexture);
|
||||
m_atlasTexture = 0;
|
||||
for (auto tex : m_atlasPages) {
|
||||
if (tex != 0) {
|
||||
glDeleteTextures(1, &tex);
|
||||
}
|
||||
}
|
||||
m_atlasPages.clear();
|
||||
m_glyphs.clear();
|
||||
m_atlasCursorX = 1;
|
||||
m_atlasCursorY = 1;
|
||||
@@ -201,20 +239,18 @@ void MsdfTextRenderer::cleanup() {
|
||||
|
||||
m_program.destroy();
|
||||
|
||||
if (m_fontHandle != nullptr) {
|
||||
msdfgen::destroyFont(m_fontHandle);
|
||||
m_fontHandle = nullptr;
|
||||
}
|
||||
|
||||
if (m_hbFont != nullptr) {
|
||||
hb_font_destroy(m_hbFont);
|
||||
m_hbFont = nullptr;
|
||||
}
|
||||
|
||||
if (m_face != nullptr) {
|
||||
FT_Done_Face(m_face);
|
||||
m_face = nullptr;
|
||||
for (auto& slot : m_fontSlots) {
|
||||
if (slot.fontHandle != nullptr) {
|
||||
msdfgen::destroyFont(slot.fontHandle);
|
||||
}
|
||||
if (slot.hbFont != nullptr) {
|
||||
hb_font_destroy(slot.hbFont);
|
||||
}
|
||||
if (slot.face != nullptr) {
|
||||
FT_Done_Face(slot.face);
|
||||
}
|
||||
}
|
||||
m_fontSlots.clear();
|
||||
|
||||
if (m_library != nullptr) {
|
||||
FT_Done_FreeType(m_library);
|
||||
@@ -222,27 +258,28 @@ void MsdfTextRenderer::cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
void MsdfTextRenderer::ensureAtlasInitialized() {
|
||||
if (m_atlasTexture != 0) {
|
||||
return;
|
||||
GLuint MsdfTextRenderer::ensureAtlasPage(std::uint32_t page) {
|
||||
while (m_atlasPages.size() <= page) {
|
||||
GLuint tex = 0;
|
||||
glGenTextures(1, &tex);
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_RGB,
|
||||
m_atlasWidth,
|
||||
m_atlasHeight,
|
||||
0,
|
||||
GL_RGB,
|
||||
GL_UNSIGNED_BYTE,
|
||||
nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
m_atlasPages.push_back(tex);
|
||||
}
|
||||
|
||||
glGenTextures(1, &m_atlasTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_atlasTexture);
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_RGB,
|
||||
m_atlasWidth,
|
||||
m_atlasHeight,
|
||||
0,
|
||||
GL_RGB,
|
||||
GL_UNSIGNED_BYTE,
|
||||
nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
return m_atlasPages[page];
|
||||
}
|
||||
|
||||
void MsdfTextRenderer::setShapingSize(float fontSize) {
|
||||
@@ -250,36 +287,154 @@ void MsdfTextRenderer::setShapingSize(float fontSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (FT_Set_Pixel_Sizes(m_face, 0, static_cast<FT_UInt>(fontSize)) != 0) {
|
||||
throw std::runtime_error("FT_Set_Pixel_Sizes failed");
|
||||
}
|
||||
for (auto& slot : m_fontSlots) {
|
||||
if (FT_Set_Pixel_Sizes(slot.face, 0, static_cast<FT_UInt>(fontSize)) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m_hbFont != nullptr) {
|
||||
hb_font_destroy(m_hbFont);
|
||||
}
|
||||
m_hbFont = hb_ft_font_create_referenced(m_face);
|
||||
if (m_hbFont == nullptr) {
|
||||
throw std::runtime_error("hb_ft_font_create_referenced failed");
|
||||
if (slot.hbFont != nullptr) {
|
||||
hb_font_destroy(slot.hbFont);
|
||||
}
|
||||
slot.hbFont = hb_ft_font_create_referenced(slot.face);
|
||||
}
|
||||
|
||||
m_currentShapingSize = fontSize;
|
||||
}
|
||||
|
||||
MsdfTextRenderer::Glyph& MsdfTextRenderer::loadGlyph(std::uint32_t glyphIndex) {
|
||||
if (auto it = m_glyphs.find(glyphIndex); it != m_glyphs.end()) {
|
||||
std::vector<MsdfTextRenderer::ShapedGlyph> MsdfTextRenderer::shapeWithFallback(
|
||||
std::string_view text, float fontSize) {
|
||||
|
||||
setShapingSize(fontSize);
|
||||
|
||||
// Shape with primary font
|
||||
hb_buffer_t* buffer = hb_buffer_create();
|
||||
hb_buffer_add_utf8(buffer, text.data(), static_cast<int>(text.size()), 0, static_cast<int>(text.size()));
|
||||
hb_buffer_guess_segment_properties(buffer);
|
||||
hb_shape(m_fontSlots[0].hbFont, buffer, nullptr, 0);
|
||||
|
||||
unsigned int glyphCount = 0;
|
||||
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(buffer, &glyphCount);
|
||||
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(buffer, &glyphCount);
|
||||
|
||||
std::vector<ShapedGlyph> result;
|
||||
result.reserve(glyphCount);
|
||||
|
||||
// Collect results, marking notdef glyphs for fallback
|
||||
std::vector<bool> needsFallback(glyphCount, false);
|
||||
bool anyNeedsFallback = false;
|
||||
|
||||
for (unsigned int i = 0; i < glyphCount; ++i) {
|
||||
result.push_back(ShapedGlyph{
|
||||
.key = makeGlyphKey(0, glyphInfos[i].codepoint),
|
||||
.slotIndex = 0,
|
||||
.glyphIndex = glyphInfos[i].codepoint,
|
||||
.position = glyphPositions[i],
|
||||
});
|
||||
|
||||
if (glyphInfos[i].codepoint == 0) {
|
||||
needsFallback[i] = true;
|
||||
anyNeedsFallback = true;
|
||||
}
|
||||
}
|
||||
|
||||
hb_buffer_destroy(buffer);
|
||||
|
||||
if (!anyNeedsFallback || m_fontSlots.size() <= 1) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// For each notdef glyph, find the original character cluster and try fallback fonts.
|
||||
// We need the original text to re-shape with fallback fonts.
|
||||
// HarfBuzz cluster values map back to byte offsets in the input text.
|
||||
// Re-extract cluster info from the primary shaping result.
|
||||
buffer = hb_buffer_create();
|
||||
hb_buffer_add_utf8(buffer, text.data(), static_cast<int>(text.size()), 0, static_cast<int>(text.size()));
|
||||
hb_buffer_guess_segment_properties(buffer);
|
||||
hb_shape(m_fontSlots[0].hbFont, buffer, nullptr, 0);
|
||||
|
||||
glyphInfos = hb_buffer_get_glyph_infos(buffer, &glyphCount);
|
||||
|
||||
for (unsigned int i = 0; i < glyphCount; ++i) {
|
||||
if (!needsFallback[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the byte range for this cluster
|
||||
const std::uint32_t clusterStart = glyphInfos[i].cluster;
|
||||
std::uint32_t clusterEnd;
|
||||
if (i + 1 < glyphCount) {
|
||||
clusterEnd = glyphInfos[i + 1].cluster;
|
||||
} else {
|
||||
clusterEnd = static_cast<std::uint32_t>(text.size());
|
||||
}
|
||||
|
||||
if (clusterEnd <= clusterStart) {
|
||||
// RTL or complex cluster ordering -- skip fallback for this glyph
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string_view cluster = text.substr(clusterStart, clusterEnd - clusterStart);
|
||||
|
||||
// Try each fallback font
|
||||
for (std::uint32_t slotIdx = 1; slotIdx < static_cast<std::uint32_t>(m_fontSlots.size()); ++slotIdx) {
|
||||
auto& slot = m_fontSlots[slotIdx];
|
||||
if (slot.hbFont == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
hb_buffer_t* fbBuf = hb_buffer_create();
|
||||
hb_buffer_add_utf8(fbBuf, cluster.data(), static_cast<int>(cluster.size()),
|
||||
0, static_cast<int>(cluster.size()));
|
||||
hb_buffer_guess_segment_properties(fbBuf);
|
||||
hb_shape(slot.hbFont, fbBuf, nullptr, 0);
|
||||
|
||||
unsigned int fbCount = 0;
|
||||
hb_glyph_info_t* fbInfos = hb_buffer_get_glyph_infos(fbBuf, &fbCount);
|
||||
hb_glyph_position_t* fbPositions = hb_buffer_get_glyph_positions(fbBuf, &fbCount);
|
||||
|
||||
// Check if this font has the glyph (not notdef)
|
||||
if (fbCount >= 1 && fbInfos[0].codepoint != 0) {
|
||||
// Replace with fallback result. For single-cluster, typically 1 glyph.
|
||||
result[i] = ShapedGlyph{
|
||||
.key = makeGlyphKey(slotIdx, fbInfos[0].codepoint),
|
||||
.slotIndex = slotIdx,
|
||||
.glyphIndex = fbInfos[0].codepoint,
|
||||
.position = fbPositions[0],
|
||||
};
|
||||
hb_buffer_destroy(fbBuf);
|
||||
break;
|
||||
}
|
||||
|
||||
hb_buffer_destroy(fbBuf);
|
||||
}
|
||||
}
|
||||
|
||||
hb_buffer_destroy(buffer);
|
||||
return result;
|
||||
}
|
||||
|
||||
MsdfTextRenderer::Glyph& MsdfTextRenderer::loadGlyph(std::uint32_t slotIndex, std::uint32_t glyphIndex) {
|
||||
const GlyphKey key = makeGlyphKey(slotIndex, glyphIndex);
|
||||
if (auto it = m_glyphs.find(key); it != m_glyphs.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
if (slotIndex >= m_fontSlots.size()) {
|
||||
auto [it, _] = m_glyphs.emplace(key, Glyph{});
|
||||
return it->second;
|
||||
}
|
||||
|
||||
auto& slot = m_fontSlots[slotIndex];
|
||||
msdfgen::Shape shape;
|
||||
double advance = 0.0;
|
||||
if (!msdfgen::loadGlyph(shape, m_fontHandle, msdfgen::GlyphIndex(glyphIndex),
|
||||
if (!msdfgen::loadGlyph(shape, slot.fontHandle, msdfgen::GlyphIndex(glyphIndex),
|
||||
msdfgen::FONT_SCALING_EM_NORMALIZED, &advance)) {
|
||||
auto [it, _] = m_glyphs.emplace(glyphIndex, Glyph{});
|
||||
auto [it, _] = m_glyphs.emplace(key, Glyph{});
|
||||
return it->second;
|
||||
}
|
||||
|
||||
if (shape.contours.empty()) {
|
||||
auto [it, _] = m_glyphs.emplace(glyphIndex, Glyph{});
|
||||
auto [it, _] = m_glyphs.emplace(key, Glyph{});
|
||||
return it->second;
|
||||
}
|
||||
|
||||
@@ -320,20 +475,29 @@ MsdfTextRenderer::Glyph& MsdfTextRenderer::loadGlyph(std::uint32_t glyphIndex) {
|
||||
const int paddedW = glyphW + kGlyphPadding * 2;
|
||||
const int paddedH = glyphH + kGlyphPadding * 2;
|
||||
|
||||
// Check if we need to advance to next row or next page
|
||||
if (m_atlasCursorX + paddedW > m_atlasWidth) {
|
||||
m_atlasCursorX = 1;
|
||||
m_atlasCursorY += m_atlasRowHeight + 1;
|
||||
m_atlasRowHeight = 0;
|
||||
}
|
||||
|
||||
std::uint32_t currentPage = m_atlasPages.empty() ? 0 : static_cast<std::uint32_t>(m_atlasPages.size() - 1);
|
||||
|
||||
if (m_atlasCursorY + paddedH > m_atlasHeight) {
|
||||
throw std::runtime_error("MSDF text atlas is full");
|
||||
// Current page is full, allocate a new one
|
||||
currentPage = static_cast<std::uint32_t>(m_atlasPages.size());
|
||||
ensureAtlasPage(currentPage);
|
||||
m_atlasCursorX = 1;
|
||||
m_atlasCursorY = 1;
|
||||
m_atlasRowHeight = 0;
|
||||
logDebug("allocated atlas page {}", currentPage);
|
||||
}
|
||||
|
||||
const int destX = m_atlasCursorX + kGlyphPadding;
|
||||
const int destY = m_atlasCursorY + kGlyphPadding;
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, m_atlasTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_atlasPages[currentPage]);
|
||||
glTexSubImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
@@ -345,20 +509,24 @@ MsdfTextRenderer::Glyph& MsdfTextRenderer::loadGlyph(std::uint32_t glyphIndex) {
|
||||
GL_UNSIGNED_BYTE,
|
||||
pixels.data());
|
||||
|
||||
const auto atlasW = static_cast<float>(m_atlasWidth);
|
||||
const auto atlasH = static_cast<float>(m_atlasHeight);
|
||||
|
||||
Glyph glyph{
|
||||
.atlasWidth = static_cast<float>(glyphW),
|
||||
.atlasHeight = static_cast<float>(glyphH),
|
||||
.bearingX = static_cast<float>(pxLeft),
|
||||
.bearingY = static_cast<float>(pxTop),
|
||||
.u0 = static_cast<float>(destX) / static_cast<float>(m_atlasWidth),
|
||||
.v0 = static_cast<float>(destY) / static_cast<float>(m_atlasHeight),
|
||||
.u1 = static_cast<float>(destX + glyphW) / static_cast<float>(m_atlasWidth),
|
||||
.v1 = static_cast<float>(destY + glyphH) / static_cast<float>(m_atlasHeight),
|
||||
.u0 = static_cast<float>(destX) / atlasW,
|
||||
.v0 = static_cast<float>(destY) / atlasH,
|
||||
.u1 = static_cast<float>(destX + glyphW) / atlasW,
|
||||
.v1 = static_cast<float>(destY + glyphH) / atlasH,
|
||||
.atlasPage = currentPage,
|
||||
};
|
||||
|
||||
m_atlasCursorX += paddedW + 1;
|
||||
m_atlasRowHeight = std::max(m_atlasRowHeight, paddedH);
|
||||
|
||||
auto [it, _] = m_glyphs.emplace(glyphIndex, glyph);
|
||||
auto [it, _] = m_glyphs.emplace(key, glyph);
|
||||
return it->second;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
struct ResolvedFont;
|
||||
|
||||
namespace msdfgen {
|
||||
class FontHandle;
|
||||
@@ -29,8 +32,14 @@ public:
|
||||
MsdfTextRenderer(const MsdfTextRenderer&) = delete;
|
||||
MsdfTextRenderer& operator=(const MsdfTextRenderer&) = delete;
|
||||
|
||||
void initialize(const char* fontPath);
|
||||
struct TruncatedText {
|
||||
std::string text;
|
||||
float width = 0.0f;
|
||||
};
|
||||
|
||||
void initialize(const std::vector<ResolvedFont>& fonts);
|
||||
[[nodiscard]] TextMetrics measure(std::string_view text, float fontSize);
|
||||
[[nodiscard]] TruncatedText truncate(std::string_view text, float fontSize, float maxWidth);
|
||||
void draw(float surfaceWidth,
|
||||
float surfaceHeight,
|
||||
float x,
|
||||
@@ -41,6 +50,12 @@ public:
|
||||
void cleanup();
|
||||
|
||||
private:
|
||||
struct FontSlot {
|
||||
FT_Face face = nullptr;
|
||||
hb_font_t* hbFont = nullptr;
|
||||
msdfgen::FontHandle* fontHandle = nullptr;
|
||||
};
|
||||
|
||||
struct Glyph {
|
||||
float atlasWidth = 0.0f;
|
||||
float atlasHeight = 0.0f;
|
||||
@@ -50,23 +65,36 @@ private:
|
||||
float v0 = 0.0f;
|
||||
float u1 = 1.0f;
|
||||
float v1 = 1.0f;
|
||||
std::uint32_t atlasPage = 0;
|
||||
};
|
||||
|
||||
Glyph& loadGlyph(std::uint32_t glyphIndex);
|
||||
void ensureAtlasInitialized();
|
||||
// Cache key: (slotIndex << 24) | glyphIndex
|
||||
using GlyphKey = std::uint64_t;
|
||||
static GlyphKey makeGlyphKey(std::uint32_t slotIndex, std::uint32_t glyphIndex) {
|
||||
return (static_cast<std::uint64_t>(slotIndex) << 32) | glyphIndex;
|
||||
}
|
||||
|
||||
struct ShapedGlyph {
|
||||
GlyphKey key;
|
||||
std::uint32_t slotIndex;
|
||||
std::uint32_t glyphIndex;
|
||||
hb_glyph_position_t position;
|
||||
};
|
||||
|
||||
Glyph& loadGlyph(std::uint32_t slotIndex, std::uint32_t glyphIndex);
|
||||
GLuint ensureAtlasPage(std::uint32_t page);
|
||||
void setShapingSize(float fontSize);
|
||||
std::vector<ShapedGlyph> shapeWithFallback(std::string_view text, float fontSize);
|
||||
|
||||
FT_Library m_library = nullptr;
|
||||
FT_Face m_face = nullptr;
|
||||
hb_font_t* m_hbFont = nullptr;
|
||||
msdfgen::FontHandle* m_fontHandle = nullptr;
|
||||
std::vector<FontSlot> m_fontSlots;
|
||||
MsdfTextProgram m_program;
|
||||
GLuint m_atlasTexture = 0;
|
||||
std::vector<GLuint> m_atlasPages;
|
||||
int m_atlasWidth = 512;
|
||||
int m_atlasHeight = 512;
|
||||
int m_atlasCursorX = 1;
|
||||
int m_atlasCursorY = 1;
|
||||
int m_atlasRowHeight = 0;
|
||||
float m_currentShapingSize = 0.0f;
|
||||
std::unordered_map<std::uint32_t, Glyph> m_glyphs;
|
||||
std::unordered_map<GlyphKey, Glyph> m_glyphs;
|
||||
};
|
||||
|
||||
@@ -20,8 +20,9 @@ public:
|
||||
[[nodiscard]] virtual const char* name() const noexcept = 0;
|
||||
|
||||
virtual void bind(wl_display* display, wl_surface* surface) = 0;
|
||||
virtual void resize(std::uint32_t width, std::uint32_t height) = 0;
|
||||
virtual void render(std::uint32_t width, std::uint32_t height) = 0;
|
||||
virtual void resize(std::uint32_t bufferWidth, std::uint32_t bufferHeight,
|
||||
std::uint32_t logicalWidth, std::uint32_t logicalHeight) = 0;
|
||||
virtual void render() = 0;
|
||||
virtual void setScene(Node* root) = 0;
|
||||
[[nodiscard]] virtual TextMetrics measureText(std::string_view text, float fontSize) = 0;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
#include "render/TextureManager.hpp"
|
||||
|
||||
#include "core/Log.hpp"
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wconversion"
|
||||
#pragma GCC diagnostic ignored "-Wsign-conversion"
|
||||
#pragma GCC diagnostic ignored "-Wfloat-conversion"
|
||||
#pragma GCC diagnostic ignored "-Wold-style-cast"
|
||||
#pragma GCC diagnostic ignored "-Wshadow"
|
||||
#include <stb_image.h>
|
||||
#include <nanosvg.h>
|
||||
#include <nanosvgrast.h>
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
bool endsWith(const std::string& str, const std::string& suffix) {
|
||||
if (suffix.size() > str.size()) {
|
||||
return false;
|
||||
}
|
||||
return std::equal(suffix.rbegin(), suffix.rend(), str.rbegin());
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> readFile(const std::string& path) {
|
||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
const auto size = file.tellg();
|
||||
if (size <= 0) {
|
||||
return {};
|
||||
}
|
||||
std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
|
||||
file.seekg(0);
|
||||
file.read(reinterpret_cast<char*>(data.data()), size);
|
||||
return data;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TextureManager::~TextureManager() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
TextureHandle TextureManager::loadFromFile(const std::string& path, int targetSize) {
|
||||
if (endsWith(path, ".svg") || endsWith(path, ".SVG")) {
|
||||
// SVG rasterization via nanosvg
|
||||
auto fileData = readFile(path);
|
||||
if (fileData.empty()) {
|
||||
logWarn("failed to read SVG: {}", path);
|
||||
return {};
|
||||
}
|
||||
|
||||
// nsvgParse needs null-terminated mutable string
|
||||
fileData.push_back(0);
|
||||
auto* image = nsvgParse(reinterpret_cast<char*>(fileData.data()), "px", 96.0f);
|
||||
if (image == nullptr) {
|
||||
logWarn("failed to parse SVG: {}", path);
|
||||
return {};
|
||||
}
|
||||
|
||||
int w = targetSize > 0 ? targetSize : static_cast<int>(image->width);
|
||||
int h = targetSize > 0 ? targetSize : static_cast<int>(image->height);
|
||||
if (w <= 0 || h <= 0) {
|
||||
nsvgDelete(image);
|
||||
return {};
|
||||
}
|
||||
|
||||
float scaleX = static_cast<float>(w) / image->width;
|
||||
float scaleY = static_cast<float>(h) / image->height;
|
||||
float scale = std::min(scaleX, scaleY);
|
||||
|
||||
auto* rast = nsvgCreateRasterizer();
|
||||
if (rast == nullptr) {
|
||||
nsvgDelete(image);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> pixels(static_cast<std::size_t>(w * h * 4));
|
||||
nsvgRasterize(rast, image, 0, 0, scale, pixels.data(), w, h, w * 4);
|
||||
nsvgDeleteRasterizer(rast);
|
||||
nsvgDelete(image);
|
||||
|
||||
return uploadRgba(pixels.data(), w, h);
|
||||
}
|
||||
|
||||
// PNG/JPEG via stb_image
|
||||
auto fileData = readFile(path);
|
||||
if (fileData.empty()) {
|
||||
logWarn("failed to read image: {}", path);
|
||||
return {};
|
||||
}
|
||||
|
||||
int w = 0, h = 0, channels = 0;
|
||||
auto* pixels = stbi_load_from_memory(
|
||||
fileData.data(), static_cast<int>(fileData.size()),
|
||||
&w, &h, &channels, 4);
|
||||
if (pixels == nullptr) {
|
||||
logWarn("failed to decode image: {}", path);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto handle = uploadRgba(pixels, w, h);
|
||||
stbi_image_free(pixels);
|
||||
return handle;
|
||||
}
|
||||
|
||||
TextureHandle TextureManager::loadFromArgbPixmap(const std::uint8_t* data, int width, int height) {
|
||||
if (data == nullptr || width <= 0 || height <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto pixelCount = static_cast<std::size_t>(width * height);
|
||||
std::vector<std::uint8_t> rgba(pixelCount * 4);
|
||||
|
||||
for (std::size_t i = 0; i < pixelCount; ++i) {
|
||||
const std::size_t srcIdx = i * 4;
|
||||
const std::size_t dstIdx = i * 4;
|
||||
// ARGB → RGBA
|
||||
rgba[dstIdx + 0] = data[srcIdx + 1]; // R
|
||||
rgba[dstIdx + 1] = data[srcIdx + 2]; // G
|
||||
rgba[dstIdx + 2] = data[srcIdx + 3]; // B
|
||||
rgba[dstIdx + 3] = data[srcIdx + 0]; // A
|
||||
}
|
||||
|
||||
return uploadRgba(rgba.data(), width, height);
|
||||
}
|
||||
|
||||
void TextureManager::unload(TextureHandle& handle) {
|
||||
if (handle.id != 0) {
|
||||
glDeleteTextures(1, &handle.id);
|
||||
std::erase(m_textures, handle.id);
|
||||
handle = {};
|
||||
}
|
||||
}
|
||||
|
||||
void TextureManager::cleanup() {
|
||||
if (!m_textures.empty()) {
|
||||
glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
|
||||
m_textures.clear();
|
||||
}
|
||||
}
|
||||
|
||||
TextureHandle TextureManager::uploadRgba(const std::uint8_t* data, int width, int height) {
|
||||
GLuint tex = 0;
|
||||
glGenTextures(1, &tex);
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, data);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
|
||||
m_textures.push_back(tex);
|
||||
return TextureHandle{.id = tex, .width = width, .height = height};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <GLES2/gl2.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct TextureHandle {
|
||||
GLuint id = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
class TextureManager {
|
||||
public:
|
||||
TextureManager() = default;
|
||||
~TextureManager();
|
||||
|
||||
TextureManager(const TextureManager&) = delete;
|
||||
TextureManager& operator=(const TextureManager&) = delete;
|
||||
|
||||
[[nodiscard]] TextureHandle loadFromFile(const std::string& path, int targetSize = 0);
|
||||
[[nodiscard]] TextureHandle loadFromArgbPixmap(const std::uint8_t* data, int width, int height);
|
||||
void unload(TextureHandle& handle);
|
||||
void cleanup();
|
||||
|
||||
private:
|
||||
TextureHandle uploadRgba(const std::uint8_t* data, int width, int height);
|
||||
std::vector<GLuint> m_textures;
|
||||
};
|
||||
@@ -13,6 +13,7 @@ public:
|
||||
[[nodiscard]] const std::string& text() const noexcept { return m_text; }
|
||||
[[nodiscard]] float fontSize() const noexcept { return m_fontSize; }
|
||||
[[nodiscard]] const Color& color() const noexcept { return m_color; }
|
||||
[[nodiscard]] float maxWidth() const noexcept { return m_maxWidth; }
|
||||
|
||||
void setText(std::string text) {
|
||||
if (m_text == text) {
|
||||
@@ -35,8 +36,17 @@ public:
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setMaxWidth(float maxWidth) {
|
||||
if (m_maxWidth == maxWidth) {
|
||||
return;
|
||||
}
|
||||
m_maxWidth = maxWidth;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_text;
|
||||
float m_fontSize = 14.0f;
|
||||
float m_maxWidth = 0.0f;
|
||||
Color m_color;
|
||||
};
|
||||
|
||||
+84
-30
@@ -1,9 +1,11 @@
|
||||
#include "shell/Bar.hpp"
|
||||
|
||||
#include "core/Log.hpp"
|
||||
#include "render/Palette.hpp"
|
||||
#include "render/scene/RectNode.hpp"
|
||||
#include "render/scene/TextNode.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <wayland-client-core.h>
|
||||
@@ -14,39 +16,30 @@ constexpr std::uint32_t kBarHeight = 36;
|
||||
|
||||
} // namespace
|
||||
|
||||
Bar::Bar()
|
||||
: m_layerSurface(m_connection, LayerSurfaceConfig{
|
||||
.nameSpace = "noctalia-bar",
|
||||
.layer = LayerShellLayer::Top,
|
||||
.anchor = LayerShellAnchor::Top | LayerShellAnchor::Left | LayerShellAnchor::Right,
|
||||
.height = kBarHeight,
|
||||
.exclusiveZone = static_cast<std::int32_t>(kBarHeight),
|
||||
.defaultHeight = kBarHeight,
|
||||
}) {}
|
||||
Bar::Bar() = default;
|
||||
|
||||
bool Bar::initialize() {
|
||||
if (!m_connection.connect()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_layerSurface.setConfigureCallback(
|
||||
[this](std::uint32_t width, std::uint32_t height) {
|
||||
buildScene(width, height);
|
||||
});
|
||||
m_connection.setOutputChangeCallback([this]() {
|
||||
syncInstances();
|
||||
});
|
||||
|
||||
m_layerSurface.initialize();
|
||||
syncInstances();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Bar::isRunning() const noexcept {
|
||||
return m_layerSurface.isRunning();
|
||||
return std::any_of(m_instances.begin(), m_instances.end(),
|
||||
[](const auto& inst) { return inst->surface && inst->surface->isRunning(); });
|
||||
}
|
||||
|
||||
int Bar::displayFd() const noexcept {
|
||||
if (!m_connection.isConnected()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return wl_display_get_fd(m_connection.display());
|
||||
}
|
||||
|
||||
@@ -54,7 +47,6 @@ void Bar::dispatchPending() {
|
||||
if (!m_connection.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (wl_display_dispatch_pending(m_connection.display()) < 0) {
|
||||
throw std::runtime_error("failed to dispatch pending Wayland events");
|
||||
}
|
||||
@@ -64,7 +56,6 @@ void Bar::dispatchReadable() {
|
||||
if (!m_connection.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (wl_display_dispatch(m_connection.display()) < 0) {
|
||||
throw std::runtime_error("failed to dispatch Wayland events");
|
||||
}
|
||||
@@ -74,7 +65,6 @@ void Bar::flush() {
|
||||
if (!m_connection.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (wl_display_flush(m_connection.display()) < 0) {
|
||||
throw std::runtime_error("failed to flush Wayland display");
|
||||
}
|
||||
@@ -84,8 +74,72 @@ const WaylandConnection& Bar::connection() const noexcept {
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
void Bar::buildScene(std::uint32_t width, std::uint32_t height) {
|
||||
auto* renderer = m_layerSurface.renderer();
|
||||
void Bar::syncInstances() {
|
||||
const auto& outputs = m_connection.outputs();
|
||||
|
||||
// Remove instances for outputs that no longer exist
|
||||
std::erase_if(m_instances, [&outputs](const auto& inst) {
|
||||
bool found = std::any_of(outputs.begin(), outputs.end(),
|
||||
[&inst](const auto& out) { return out.name == inst->outputName; });
|
||||
if (!found) {
|
||||
logInfo("bar: removing instance for output {}", inst->outputName);
|
||||
}
|
||||
return !found;
|
||||
});
|
||||
|
||||
// Create instances for new outputs
|
||||
for (const auto& output : outputs) {
|
||||
bool exists = std::any_of(m_instances.begin(), m_instances.end(),
|
||||
[&output](const auto& inst) { return inst->outputName == output.name; });
|
||||
if (!exists) {
|
||||
createInstance(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Bar::createInstance(const WaylandOutput& output) {
|
||||
logInfo("bar: creating instance for output {} scale={}", output.name, output.scale);
|
||||
|
||||
auto instance = std::make_unique<BarInstance>();
|
||||
instance->outputName = output.name;
|
||||
instance->output = output.output;
|
||||
instance->scale = output.scale;
|
||||
|
||||
auto config = LayerSurfaceConfig{
|
||||
.nameSpace = "noctalia-bar",
|
||||
.layer = LayerShellLayer::Top,
|
||||
.anchor = LayerShellAnchor::Top | LayerShellAnchor::Left | LayerShellAnchor::Right,
|
||||
.height = kBarHeight,
|
||||
.exclusiveZone = static_cast<std::int32_t>(kBarHeight),
|
||||
.defaultHeight = kBarHeight,
|
||||
};
|
||||
|
||||
instance->surface = std::make_unique<LayerSurface>(m_connection, std::move(config));
|
||||
|
||||
auto* inst = instance.get();
|
||||
instance->surface->setConfigureCallback(
|
||||
[this, inst](std::uint32_t width, std::uint32_t height) {
|
||||
buildScene(*inst, width, height);
|
||||
});
|
||||
|
||||
instance->surface->setAnimationManager(&instance->animations);
|
||||
|
||||
if (!instance->surface->initialize(output.output, output.scale)) {
|
||||
logWarn("bar: failed to initialize surface for output {}", output.name);
|
||||
return;
|
||||
}
|
||||
|
||||
m_instances.push_back(std::move(instance));
|
||||
}
|
||||
|
||||
void Bar::destroyInstance(std::uint32_t outputName) {
|
||||
std::erase_if(m_instances, [outputName](const auto& inst) {
|
||||
return inst->outputName == outputName;
|
||||
});
|
||||
}
|
||||
|
||||
void Bar::buildScene(BarInstance& instance, std::uint32_t width, std::uint32_t height) {
|
||||
auto* renderer = instance.surface->renderer();
|
||||
if (renderer == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -93,8 +147,8 @@ void Bar::buildScene(std::uint32_t width, std::uint32_t height) {
|
||||
const auto w = static_cast<float>(width);
|
||||
const auto h = static_cast<float>(height);
|
||||
|
||||
if (m_sceneRoot == nullptr) {
|
||||
m_sceneRoot = std::make_unique<Node>();
|
||||
if (instance.sceneRoot == nullptr) {
|
||||
instance.sceneRoot = std::make_unique<Node>();
|
||||
|
||||
auto bg = std::make_unique<RectNode>();
|
||||
bg->setStyle(RoundedRectStyle{
|
||||
@@ -111,7 +165,7 @@ void Bar::buildScene(std::uint32_t width, std::uint32_t height) {
|
||||
.softness = 1.2f,
|
||||
.borderWidth = 1.0f,
|
||||
});
|
||||
m_sceneRoot->addChild(std::move(bg));
|
||||
instance.sceneRoot->addChild(std::move(bg));
|
||||
|
||||
auto accent = std::make_unique<RectNode>();
|
||||
accent->setPosition(18.0f, 12.0f);
|
||||
@@ -122,28 +176,28 @@ void Bar::buildScene(std::uint32_t width, std::uint32_t height) {
|
||||
.fillMode = FillMode::LinearGradient,
|
||||
.gradientDirection = GradientDirection::Horizontal,
|
||||
});
|
||||
m_sceneRoot->addChild(std::move(accent));
|
||||
instance.sceneRoot->addChild(std::move(accent));
|
||||
|
||||
auto label = std::make_unique<TextNode>();
|
||||
label->setText("Noctalia");
|
||||
label->setFontSize(14.0f);
|
||||
label->setColor(kRosePinePalette.text);
|
||||
m_labelNode = static_cast<TextNode*>(m_sceneRoot->addChild(std::move(label)));
|
||||
instance.labelNode = static_cast<TextNode*>(instance.sceneRoot->addChild(std::move(label)));
|
||||
|
||||
renderer->setScene(m_sceneRoot.get());
|
||||
renderer->setScene(instance.sceneRoot.get());
|
||||
}
|
||||
|
||||
// Update size-dependent layout
|
||||
auto& children = m_sceneRoot->children();
|
||||
auto& children = instance.sceneRoot->children();
|
||||
|
||||
// Background rect
|
||||
children[0]->setPosition(10.0f, 6.0f);
|
||||
children[0]->setSize(w - 20.0f, h - 12.0f);
|
||||
|
||||
// Center text label
|
||||
const auto metrics = renderer->measureText(m_labelNode->text(), m_labelNode->fontSize());
|
||||
const auto metrics = renderer->measureText(instance.labelNode->text(), instance.labelNode->fontSize());
|
||||
const float labelX = (w - metrics.width) * 0.5f;
|
||||
const float labelHeight = metrics.bottom - metrics.top;
|
||||
const float labelBaseline = (h - labelHeight) * 0.5f - metrics.top;
|
||||
m_labelNode->setPosition(labelX, labelBaseline);
|
||||
instance.labelNode->setPosition(labelX, labelBaseline);
|
||||
}
|
||||
|
||||
+8
-8
@@ -1,12 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "render/scene/Node.hpp"
|
||||
#include "wayland/LayerSurface.hpp"
|
||||
#include "shell/BarInstance.hpp"
|
||||
#include "wayland/WaylandConnection.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
class TextNode;
|
||||
#include <vector>
|
||||
|
||||
class Bar {
|
||||
public:
|
||||
@@ -21,10 +20,11 @@ public:
|
||||
const WaylandConnection& connection() const noexcept;
|
||||
|
||||
private:
|
||||
void buildScene(std::uint32_t width, std::uint32_t height);
|
||||
void syncInstances();
|
||||
void createInstance(const WaylandOutput& output);
|
||||
void destroyInstance(std::uint32_t outputName);
|
||||
void buildScene(BarInstance& instance, std::uint32_t width, std::uint32_t height);
|
||||
|
||||
WaylandConnection m_connection;
|
||||
LayerSurface m_layerSurface;
|
||||
std::unique_ptr<Node> m_sceneRoot;
|
||||
TextNode* m_labelNode = nullptr;
|
||||
std::vector<std::unique_ptr<BarInstance>> m_instances;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "render/AnimationManager.hpp"
|
||||
#include "render/scene/Node.hpp"
|
||||
#include "wayland/LayerSurface.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
class TextNode;
|
||||
|
||||
struct BarInstance {
|
||||
std::uint32_t outputName = 0;
|
||||
wl_output* output = nullptr;
|
||||
std::int32_t scale = 1;
|
||||
std::unique_ptr<LayerSurface> surface;
|
||||
std::unique_ptr<Node> sceneRoot;
|
||||
AnimationManager animations;
|
||||
TextNode* labelNode = nullptr;
|
||||
};
|
||||
@@ -29,6 +29,16 @@ LayerSurface::~LayerSurface() {
|
||||
}
|
||||
|
||||
bool LayerSurface::initialize() {
|
||||
wl_output* output = nullptr;
|
||||
std::int32_t scale = 1;
|
||||
if (!m_connection.outputs().empty()) {
|
||||
output = m_connection.outputs().front().output;
|
||||
scale = m_connection.outputs().front().scale;
|
||||
}
|
||||
return initialize(output, scale);
|
||||
}
|
||||
|
||||
bool LayerSurface::initialize(wl_output* output, std::int32_t scale) {
|
||||
if (!m_connection.hasRequiredGlobals()) {
|
||||
logWarn("layer surface skipped: missing compositor/shm/layer-shell globals");
|
||||
return false;
|
||||
@@ -38,10 +48,7 @@ bool LayerSurface::initialize() {
|
||||
return false;
|
||||
}
|
||||
|
||||
wl_output* output = nullptr;
|
||||
if (!m_connection.outputs().empty()) {
|
||||
output = m_connection.outputs().front().output;
|
||||
}
|
||||
setScale(scale);
|
||||
|
||||
m_layerSurface = zwlr_layer_shell_v1_get_layer_surface(
|
||||
m_connection.layerShell(),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
struct wl_output;
|
||||
struct zwlr_layer_surface_v1;
|
||||
|
||||
enum class LayerShellLayer : std::uint32_t {
|
||||
@@ -49,6 +50,7 @@ public:
|
||||
~LayerSurface() override;
|
||||
|
||||
bool initialize() override;
|
||||
bool initialize(wl_output* output, std::int32_t scale);
|
||||
|
||||
static void handleConfigure(void* data,
|
||||
zwlr_layer_surface_v1* layerSurface,
|
||||
|
||||
+25
-3
@@ -1,5 +1,6 @@
|
||||
#include "wayland/Surface.hpp"
|
||||
|
||||
#include "render/AnimationManager.hpp"
|
||||
#include "render/GlRenderer.hpp"
|
||||
#include "wayland/WaylandConnection.hpp"
|
||||
|
||||
@@ -27,7 +28,7 @@ bool Surface::isRunning() const noexcept {
|
||||
|
||||
void Surface::handleFrameDone(void* data,
|
||||
wl_callback* callback,
|
||||
std::uint32_t /*callbackData*/) {
|
||||
std::uint32_t callbackData) {
|
||||
auto* self = static_cast<Surface*>(data);
|
||||
|
||||
if (callback != nullptr) {
|
||||
@@ -36,6 +37,21 @@ void Surface::handleFrameDone(void* data,
|
||||
|
||||
self->m_frameCallback = nullptr;
|
||||
|
||||
if (self->m_animationManager != nullptr) {
|
||||
float deltaMs = 0.0f;
|
||||
if (self->m_lastFrameTime != 0 && callbackData > self->m_lastFrameTime) {
|
||||
deltaMs = static_cast<float>(callbackData - self->m_lastFrameTime);
|
||||
}
|
||||
self->m_lastFrameTime = callbackData;
|
||||
self->m_animationManager->tick(deltaMs);
|
||||
|
||||
if (self->m_animationManager->hasActive() && self->m_running && self->m_configured) {
|
||||
self->render();
|
||||
self->requestFrame();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (self->m_running && self->m_configured) {
|
||||
self->requestFrame();
|
||||
}
|
||||
@@ -62,7 +78,13 @@ void Surface::onConfigure(std::uint32_t width, std::uint32_t height) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_renderer->resize(m_width, m_height);
|
||||
if (m_scale > 1) {
|
||||
wl_surface_set_buffer_scale(m_surface, m_scale);
|
||||
}
|
||||
|
||||
const auto bufferWidth = m_width * static_cast<std::uint32_t>(m_scale);
|
||||
const auto bufferHeight = m_height * static_cast<std::uint32_t>(m_scale);
|
||||
m_renderer->resize(bufferWidth, bufferHeight, m_width, m_height);
|
||||
if (m_configureCallback) {
|
||||
m_configureCallback(m_width, m_height);
|
||||
}
|
||||
@@ -83,7 +105,7 @@ void Surface::render() {
|
||||
}
|
||||
|
||||
requestFrame();
|
||||
m_renderer->render(m_width, m_height);
|
||||
m_renderer->render();
|
||||
}
|
||||
|
||||
void Surface::requestFrame() {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
struct wl_callback;
|
||||
struct wl_surface;
|
||||
|
||||
class AnimationManager;
|
||||
class WaylandConnection;
|
||||
|
||||
class Surface {
|
||||
@@ -26,9 +27,11 @@ public:
|
||||
[[nodiscard]] bool isRunning() const noexcept;
|
||||
|
||||
void setConfigureCallback(ConfigureCallback callback);
|
||||
void setAnimationManager(AnimationManager* manager) noexcept { m_animationManager = manager; }
|
||||
[[nodiscard]] Renderer* renderer() const noexcept;
|
||||
[[nodiscard]] std::uint32_t width() const noexcept { return m_width; }
|
||||
[[nodiscard]] std::uint32_t height() const noexcept { return m_height; }
|
||||
[[nodiscard]] std::int32_t scale() const noexcept { return m_scale; }
|
||||
|
||||
static void handleFrameDone(void* data,
|
||||
wl_callback* callback,
|
||||
@@ -42,16 +45,20 @@ protected:
|
||||
void destroySurface();
|
||||
|
||||
void setRunning(bool running) noexcept { m_running = running; }
|
||||
void setScale(std::int32_t scale) noexcept { m_scale = scale; }
|
||||
|
||||
WaylandConnection& m_connection;
|
||||
wl_surface* m_surface = nullptr;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Renderer> m_renderer;
|
||||
AnimationManager* m_animationManager = nullptr;
|
||||
ConfigureCallback m_configureCallback;
|
||||
wl_callback* m_frameCallback = nullptr;
|
||||
std::uint32_t m_lastFrameTime = 0;
|
||||
bool m_running = false;
|
||||
bool m_configured = false;
|
||||
std::uint32_t m_width = 0;
|
||||
std::uint32_t m_height = 0;
|
||||
std::int32_t m_scale = 1;
|
||||
};
|
||||
|
||||
@@ -21,12 +21,64 @@ constexpr std::uint32_t kShmVersion = 1;
|
||||
constexpr std::uint32_t kLayerShellVersion = 4;
|
||||
#endif
|
||||
constexpr std::uint32_t kXdgOutputManagerVersion = 3;
|
||||
constexpr std::uint32_t kOutputVersion = 4;
|
||||
|
||||
const wl_registry_listener kRegistryListener = {
|
||||
.global = &WaylandConnection::handleGlobal,
|
||||
.global_remove = &WaylandConnection::handleGlobalRemove,
|
||||
};
|
||||
|
||||
void outputGeometry(void* /*data*/, wl_output* /*output*/,
|
||||
int32_t /*x*/, int32_t /*y*/,
|
||||
int32_t /*physW*/, int32_t /*physH*/,
|
||||
int32_t /*subpixel*/,
|
||||
const char* /*make*/, const char* /*model*/,
|
||||
int32_t /*transform*/) {}
|
||||
|
||||
void outputMode(void* data, wl_output* wlOut,
|
||||
uint32_t flags, int32_t w, int32_t h, int32_t /*refresh*/) {
|
||||
if ((flags & WL_OUTPUT_MODE_CURRENT) == 0) {
|
||||
return;
|
||||
}
|
||||
auto* out = static_cast<WaylandConnection*>(data)->findOutputByWl(wlOut);
|
||||
if (out != nullptr) {
|
||||
out->width = w;
|
||||
out->height = h;
|
||||
}
|
||||
}
|
||||
|
||||
void outputDone(void* data, wl_output* wlOut) {
|
||||
auto* out = static_cast<WaylandConnection*>(data)->findOutputByWl(wlOut);
|
||||
if (out != nullptr) {
|
||||
out->done = true;
|
||||
}
|
||||
}
|
||||
|
||||
void outputScale(void* data, wl_output* wlOut, int32_t factor) {
|
||||
auto* out = static_cast<WaylandConnection*>(data)->findOutputByWl(wlOut);
|
||||
if (out != nullptr) {
|
||||
out->scale = factor;
|
||||
}
|
||||
}
|
||||
|
||||
void outputName(void* /*data*/, wl_output* /*output*/, const char* /*name*/) {}
|
||||
|
||||
void outputDescription(void* data, wl_output* wlOut, const char* desc) {
|
||||
auto* out = static_cast<WaylandConnection*>(data)->findOutputByWl(wlOut);
|
||||
if (out != nullptr) {
|
||||
out->description = desc;
|
||||
}
|
||||
}
|
||||
|
||||
const wl_output_listener kOutputListener = {
|
||||
.geometry = outputGeometry,
|
||||
.mode = outputMode,
|
||||
.done = outputDone,
|
||||
.scale = outputScale,
|
||||
.name = outputName,
|
||||
.description = outputDescription,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
WaylandConnection::WaylandConnection() = default;
|
||||
@@ -70,6 +122,10 @@ bool WaylandConnection::connect() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void WaylandConnection::setOutputChangeCallback(OutputChangeCallback callback) {
|
||||
m_outputChangeCallback = std::move(callback);
|
||||
}
|
||||
|
||||
bool WaylandConnection::isConnected() const noexcept {
|
||||
return m_display != nullptr;
|
||||
}
|
||||
@@ -119,9 +175,13 @@ void WaylandConnection::handleGlobalRemove(void* data,
|
||||
wl_registry* /*registry*/,
|
||||
std::uint32_t name) {
|
||||
auto* self = static_cast<WaylandConnection*>(data);
|
||||
const auto sizeBefore = self->m_outputs.size();
|
||||
std::erase_if(self->m_outputs, [name](const WaylandOutput& output) {
|
||||
return output.name == name;
|
||||
});
|
||||
if (self->m_outputs.size() != sizeBefore && self->m_outputChangeCallback) {
|
||||
self->m_outputChangeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandConnection::bindGlobal(wl_registry* registry,
|
||||
@@ -169,17 +229,32 @@ void WaylandConnection::bindGlobal(wl_registry* registry,
|
||||
}
|
||||
|
||||
if (interfaceName == wl_output_interface.name) {
|
||||
const auto bindVersion = std::min(version, kOutputVersion);
|
||||
auto* output = static_cast<wl_output*>(
|
||||
wl_registry_bind(registry, name, &wl_output_interface, 1));
|
||||
wl_registry_bind(registry, name, &wl_output_interface, bindVersion));
|
||||
m_outputs.push_back(WaylandOutput{
|
||||
.name = name,
|
||||
.interfaceName = interfaceName,
|
||||
.description = {},
|
||||
.version = version,
|
||||
.output = output,
|
||||
});
|
||||
wl_output_add_listener(output, &kOutputListener, this);
|
||||
if (m_outputChangeCallback) {
|
||||
m_outputChangeCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WaylandOutput* WaylandConnection::findOutputByWl(wl_output* wlOutput) {
|
||||
for (auto& out : m_outputs) {
|
||||
if (out.output == wlOutput) {
|
||||
return &out;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void WaylandConnection::cleanup() {
|
||||
if (m_xdgOutputManager != nullptr) {
|
||||
zxdg_output_manager_v1_destroy(m_xdgOutputManager);
|
||||
@@ -237,6 +312,8 @@ void WaylandConnection::logStartupSummary() const {
|
||||
m_outputs.size());
|
||||
|
||||
for (const auto& output : m_outputs) {
|
||||
logInfo("output global={} version={}", output.name, output.version);
|
||||
logInfo("output global={} version={} scale={} mode={}x{} desc=\"{}\"",
|
||||
output.name, output.version, output.scale,
|
||||
output.width, output.height, output.description);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -16,8 +17,13 @@ struct zxdg_output_manager_v1;
|
||||
struct WaylandOutput {
|
||||
std::uint32_t name = 0;
|
||||
std::string interfaceName;
|
||||
std::string description;
|
||||
std::uint32_t version = 0;
|
||||
wl_output* output = nullptr;
|
||||
std::int32_t scale = 1;
|
||||
std::int32_t width = 0;
|
||||
std::int32_t height = 0;
|
||||
bool done = false;
|
||||
};
|
||||
|
||||
class WaylandConnection {
|
||||
@@ -28,7 +34,10 @@ public:
|
||||
WaylandConnection(const WaylandConnection&) = delete;
|
||||
WaylandConnection& operator=(const WaylandConnection&) = delete;
|
||||
|
||||
using OutputChangeCallback = std::function<void()>;
|
||||
|
||||
bool connect();
|
||||
void setOutputChangeCallback(OutputChangeCallback callback);
|
||||
|
||||
bool isConnected() const noexcept;
|
||||
bool hasRequiredGlobals() const noexcept;
|
||||
@@ -39,6 +48,7 @@ public:
|
||||
wl_shm* shm() const noexcept;
|
||||
zwlr_layer_shell_v1* layerShell() const noexcept;
|
||||
const std::vector<WaylandOutput>& outputs() const noexcept;
|
||||
WaylandOutput* findOutputByWl(wl_output* wlOutput);
|
||||
static void handleGlobal(void* data,
|
||||
wl_registry* registry,
|
||||
std::uint32_t name,
|
||||
@@ -65,4 +75,5 @@ private:
|
||||
zxdg_output_manager_v1* m_xdgOutputManager = nullptr;
|
||||
bool m_hasLayerShellGlobal = false;
|
||||
std::vector<WaylandOutput> m_outputs;
|
||||
OutputChangeCallback m_outputChangeCallback;
|
||||
};
|
||||
|
||||
Vendored
+3132
File diff suppressed because it is too large
Load Diff
Vendored
+1472
File diff suppressed because it is too large
Load Diff
Vendored
+7988
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user