feat(renderer): complete Phase 1 — font fallback, images, animations, multi-monitor

This commit is contained in:
Lemmy
2026-04-03 09:33:23 -04:00
parent 7e1b4dd674
commit c8bbc5b2ba
31 changed files with 13851 additions and 203 deletions
+7
View File
@@ -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}
+5 -5
View File
@@ -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
+60
View File
@@ -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;
}
+7
View File
@@ -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;
+47
View File
@@ -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;
}
+27
View File
@@ -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;
};
+60
View File
@@ -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();
}
+30
View File
@@ -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
View File
@@ -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);
+11 -4
View File
@@ -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;
};
+21
View File
@@ -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
+129
View File
@@ -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);
}
+38
View File
@@ -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
View File
@@ -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;
}
+36 -8
View File
@@ -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;
};
+3 -2
View File
@@ -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;
};
+165
View File
@@ -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};
}
+31
View File
@@ -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;
};
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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;
};
+20
View File
@@ -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;
};
+11 -4
View File
@@ -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(),
+2
View File
@@ -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
View File
@@ -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() {
+7
View File
@@ -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;
};
+79 -2
View File
@@ -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);
}
}
+11
View File
@@ -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;
};
+3132
View File
File diff suppressed because it is too large Load Diff
+1472
View File
File diff suppressed because it is too large Load Diff
+7988
View File
File diff suppressed because it is too large Load Diff