feat(icons): added tabler icons loading and drawing

This commit is contained in:
Lemmy
2026-04-03 22:05:17 -04:00
parent c29ae0e44a
commit 88c5f0d0ff
18 changed files with 6427 additions and 2 deletions
+7 -1
View File
@@ -210,8 +210,11 @@ add_executable(noctalia
src/ui/controls/Box.cpp
src/ui/controls/Button.cpp
src/ui/controls/Chip.cpp
src/ui/controls/Icon.cpp
src/ui/controls/Label.cpp
src/ui/icons/IconRegistry.cpp
src/ui/widgets/ClockWidget.cpp
src/ui/widgets/NotificationWidget.cpp
src/ui/widgets/SpacerWidget.cpp
src/ui/widgets/WorkspacesWidget.cpp
src/wayland/Surface.cpp
@@ -223,7 +226,10 @@ add_executable(noctalia
"${EXT_WORKSPACE_PROTOCOL_C}"
"${CURSOR_SHAPE_PROTOCOL_C}"
)
target_compile_definitions(noctalia PRIVATE NOCTALIA_HAVE_WLR_LAYER_SHELL=1)
target_compile_definitions(noctalia PRIVATE
NOCTALIA_HAVE_WLR_LAYER_SHELL=1
NOCTALIA_ASSETS_DIR="${CMAKE_SOURCE_DIR}/assets"
)
add_dependencies(noctalia noctalia_wayland_protocols)
File diff suppressed because it is too large Load Diff
Binary file not shown.
+1 -1
View File
@@ -28,7 +28,7 @@ struct BarConfig {
float gap = 8.0f;
std::vector<std::string> startWidgets = {};
std::vector<std::string> centerWidgets = {"workspaces"};
std::vector<std::string> endWidgets = {"clock"};
std::vector<std::string> endWidgets = {"notifications", "clock"};
std::vector<BarMonitorOverride> monitorOverrides;
};
+17
View File
@@ -1,4 +1,5 @@
#include "render/GlRenderer.hpp"
#include "render/scene/IconNode.hpp"
#include "render/scene/Node.hpp"
#include "render/scene/RectNode.hpp"
#include "render/scene/TextNode.hpp"
@@ -126,6 +127,7 @@ void GlRenderer::resize(std::uint32_t bufferWidth, std::uint32_t bufferHeight,
m_roundedRectProgram.ensureInitialized();
const auto fonts = m_fontService.resolveFallbackChain("sans-serif");
m_textRenderer.initialize(fonts);
m_iconTextRenderer.initialize({{NOCTALIA_ASSETS_DIR "/fonts/tabler-icons.ttf", 0}});
}
void GlRenderer::render() {
@@ -166,6 +168,11 @@ TextMetrics GlRenderer::measureText(std::string_view text, float fontSize) {
return TextMetrics{.width = m.width, .top = m.top, .bottom = m.bottom};
}
TextMetrics GlRenderer::measureIcon(std::string_view text, float fontSize) {
auto m = m_iconTextRenderer.measure(text, fontSize);
return TextMetrics{.width = m.width, .top = m.top, .bottom = m.bottom};
}
void GlRenderer::renderNode(const Node* node, float parentX, float parentY, float parentOpacity) {
if (!node->visible()) {
return;
@@ -212,6 +219,15 @@ void GlRenderer::renderNode(const Node* node, float parentX, float parentY, floa
}
break;
}
case NodeType::Icon: {
const auto* icon = static_cast<const IconNode*>(node);
if (!icon->text().empty()) {
auto color = icon->color();
color.a *= effectiveOpacity;
m_iconTextRenderer.draw(sw, sh, absX, absY, icon->text(), icon->fontSize(), color);
}
break;
}
case NodeType::Base:
break;
}
@@ -227,6 +243,7 @@ void GlRenderer::cleanup() {
m_linearGradientProgram.destroy();
m_roundedRectProgram.destroy();
m_textRenderer.cleanup();
m_iconTextRenderer.cleanup();
m_bufferWidth = 0;
m_bufferHeight = 0;
m_logicalWidth = 0;
+2
View File
@@ -27,6 +27,7 @@ public:
void render() override;
void setScene(Node* root) override;
[[nodiscard]] TextMetrics measureText(std::string_view text, float fontSize) override;
[[nodiscard]] TextMetrics measureIcon(std::string_view text, float fontSize) override;
[[nodiscard]] TextureManager& textureManager() override;
private:
@@ -45,6 +46,7 @@ private:
LinearGradientProgram m_linearGradientProgram;
RoundedRectProgram m_roundedRectProgram;
MsdfTextRenderer m_textRenderer;
MsdfTextRenderer m_iconTextRenderer;
TextureManager m_textureManager;
Node* m_sceneRoot = nullptr;
std::uint32_t m_bufferWidth = 0;
+4
View File
@@ -158,6 +158,10 @@ TextMetrics WallpaperRenderer::measureText(std::string_view /*text*/, float /*fo
return {};
}
TextMetrics WallpaperRenderer::measureIcon(std::string_view /*text*/, float /*fontSize*/) {
return {};
}
TextureManager& WallpaperRenderer::textureManager() {
return m_textureManager;
}
+1
View File
@@ -23,6 +23,7 @@ public:
void render() override;
void setScene(Node* root) override;
[[nodiscard]] TextMetrics measureText(std::string_view text, float fontSize) override;
[[nodiscard]] TextMetrics measureIcon(std::string_view text, float fontSize) override;
[[nodiscard]] TextureManager& textureManager() override;
// Wallpaper-specific: set state before render() is called by Surface
+1
View File
@@ -27,5 +27,6 @@ public:
virtual void render() = 0;
virtual void setScene(Node* root) = 0;
[[nodiscard]] virtual TextMetrics measureText(std::string_view text, float fontSize) = 0;
[[nodiscard]] virtual TextMetrics measureIcon(std::string_view text, float fontSize) = 0;
[[nodiscard]] virtual TextureManager& textureManager() = 0;
};
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include "render/core/Color.hpp"
#include "render/scene/Node.hpp"
#include <string>
class IconNode : public Node {
public:
IconNode()
: Node(NodeType::Icon) {}
[[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; }
void setCodepoint(char32_t codepoint) {
// Encode as UTF-8
std::string encoded;
if (codepoint <= 0x7F) {
encoded += static_cast<char>(codepoint);
} else if (codepoint <= 0x7FF) {
encoded += static_cast<char>(0xC0 | (codepoint >> 6));
encoded += static_cast<char>(0x80 | (codepoint & 0x3F));
} else if (codepoint <= 0xFFFF) {
encoded += static_cast<char>(0xE0 | (codepoint >> 12));
encoded += static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F));
encoded += static_cast<char>(0x80 | (codepoint & 0x3F));
} else if (codepoint <= 0x10FFFF) {
encoded += static_cast<char>(0xF0 | (codepoint >> 18));
encoded += static_cast<char>(0x80 | ((codepoint >> 12) & 0x3F));
encoded += static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F));
encoded += static_cast<char>(0x80 | (codepoint & 0x3F));
}
if (m_text == encoded) {
return;
}
m_text = std::move(encoded);
markDirty();
}
void setFontSize(float size) {
if (m_fontSize == size) {
return;
}
m_fontSize = size;
markDirty();
}
void setColor(const Color& color) {
m_color = color;
markDirty();
}
private:
std::string m_text;
float m_fontSize = 16.0f;
Color m_color;
};
+1
View File
@@ -9,6 +9,7 @@ enum class NodeType : std::uint8_t {
Rect,
Text,
Image,
Icon,
};
class Node {
+5
View File
@@ -3,6 +3,7 @@
#include "config/ConfigService.hpp"
#include "core/Log.hpp"
#include "ui/widgets/ClockWidget.hpp"
#include "ui/widgets/NotificationWidget.hpp"
#include "ui/widgets/SpacerWidget.hpp"
#include "ui/widgets/WorkspacesWidget.hpp"
@@ -24,6 +25,10 @@ std::unique_ptr<Widget> WidgetFactory::create(const std::string& name, wl_output
return std::make_unique<WorkspacesWidget>(m_wayland, output);
}
if (name == "notifications") {
return std::make_unique<NotificationWidget>();
}
if (name == "spacer") {
return std::make_unique<SpacerWidget>(8.0f);
}
+41
View File
@@ -0,0 +1,41 @@
#include "ui/controls/Icon.hpp"
#include "render/core/Renderer.hpp"
#include "render/scene/IconNode.hpp"
#include "ui/icons/IconRegistry.hpp"
#include "ui/style/Palette.hpp"
#include "ui/style/Style.hpp"
#include <memory>
Icon::Icon() {
auto iconNode = std::make_unique<IconNode>();
m_iconNode = static_cast<IconNode*>(addChild(std::move(iconNode)));
m_iconNode->setFontSize(Style::fontSizeSm);
m_iconNode->setColor(palette.onSurface);
}
void Icon::setIcon(std::string_view name) {
char32_t cp = IconRegistry::lookup(name);
if (cp != 0) {
m_iconNode->setCodepoint(cp);
}
}
void Icon::setCodepoint(char32_t codepoint) {
m_iconNode->setCodepoint(codepoint);
}
void Icon::setSize(float size) {
m_iconNode->setFontSize(size);
}
void Icon::setColor(const Color& color) {
m_iconNode->setColor(color);
}
void Icon::measure(Renderer& renderer) {
auto metrics = renderer.measureIcon(m_iconNode->text(), m_iconNode->fontSize());
Node::setSize(metrics.width, metrics.bottom - metrics.top);
m_iconNode->setPosition(0.0f, -metrics.top);
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "render/core/Color.hpp"
#include "render/scene/Node.hpp"
#include <string_view>
class IconNode;
class Renderer;
class Icon : public Node {
public:
Icon();
void setIcon(std::string_view name);
void setCodepoint(char32_t codepoint);
void setSize(float size);
void setColor(const Color& color);
void measure(Renderer& renderer);
private:
IconNode* m_iconNode = nullptr;
};
+208
View File
@@ -0,0 +1,208 @@
#include "ui/icons/IconRegistry.hpp"
#include <string>
#include <unordered_map>
namespace {
// Hand-curated alias → codepoint map.
// To add a new icon, find its codepoint in assets/fonts/tabler-icons.json.
// clang-format off
const std::unordered_map<std::string, char32_t> kIcons = {
// General
{"close", 0xEB55}, // x
{"check", 0xEA5E}, // check
{"settings", 0xEB20}, // settings
{"refresh", 0xEB13}, // refresh
{"add", 0xEB0B}, // plus
{"trash", 0xEB41}, // trash
{"menu", 0xEC42}, // menu-2
{"person", 0xEB4D}, // user
{"folder-open", 0xFAF7}, // folder-open
{"download", 0xEA96}, // download
{"search", 0xEB1C}, // search
{"question-mark", 0xEC9D}, // question-mark
{"info", 0xF028}, // file-description
{"eye", 0xEA9A}, // eye
{"pin", 0xEC9C}, // pin
{"unpin", 0xED5F}, // pinned-off
{"image", 0xEB0A}, // photo
{"keyboard", 0xEBD6}, // keyboard
{"star", 0xEB2E}, // star
{"star-off", 0xED62}, // star-off
{"plugin", 0xF00A}, // plug-connected
{"official-plugin", 0xF69F}, // shield-filled
// Toast / warnings
{"toast-notice", 0xEA67}, // circle-check
{"toast-warning", 0xEA05}, // alert-circle
{"toast-error", 0xEA6A}, // circle-x
{"warning", 0xF634}, // exclamation-circle
// Media
{"media-pause", 0xF690}, // player-pause-filled
{"media-play", 0xF691}, // player-play-filled
{"media-prev", 0xF693}, // player-skip-back-filled
{"media-next", 0xF694}, // player-skip-forward-filled
{"stop", 0xF695}, // player-stop-filled
{"disc", 0x1003E}, // disc-filled
{"microphone", 0xEAF0}, // microphone
{"microphone-mute", 0xED16}, // microphone-off
// Volume
{"volume-high", 0xEB51}, // volume
{"volume-low", 0xEB4F}, // volume-2
{"volume-mute", 0xF1C3}, // volume-off
{"volume-x", 0xEB50}, // volume-3
{"volume-zero", 0xEB50}, // volume-3
// Network speed
{"download-speed", 0xEA96}, // download
{"upload-speed", 0xEB47}, // upload
// System monitor
{"cpu-intensive", 0xECC6}, // alert-octagon
{"cpu-usage", 0xFA77}, // brand-speedtest
{"cpu-temperature", 0xEC2C}, // flame
{"gpu-temperature", 0xEA89}, // device-desktop
{"memory", 0xEF8E}, // cpu
{"storage", 0xEA88}, // database
{"busy", 0xF146}, // hourglass-empty
// Power
{"performance", 0xEAB1}, // gauge
{"balanced", 0xEBC2}, // scale
{"powersaver", 0xED4F}, // leaf
{"shutdown", 0xEB0D}, // power
{"lock", 0xEAE2}, // lock
{"lock-pause", 0xF92E}, // lock-pause
{"logout", 0xEBA8}, // logout
{"reboot", 0xEB13}, // refresh
{"suspend", 0xED45}, // player-pause
{"hibernate", 0xF228}, // zzz
// Night light / dark mode
{"nightlight-on", 0xEAF8}, // moon
{"nightlight-off", 0xF162}, // moon-off
{"nightlight-forced", 0xECE7}, // moon-stars
{"dark-mode", 0xFE56}, // contrast-filled
// Notifications
{"bell", 0xEA35}, // bell
{"bell-off", 0xECE9}, // bell-off
// Idle inhibitor
{"keep-awake-on", 0xEAFB}, // mug
{"keep-awake-off", 0xF165}, // mug-off
// Brightness
{"brightness-low", 0xFB23}, // brightness-down-filled
{"brightness-high", 0xFB24}, // brightness-up-filled
// Chevrons / carets
{"chevron-left", 0xEA60}, // chevron-left
{"chevron-right", 0xEA61}, // chevron-right
{"chevron-up", 0xEA62}, // chevron-up
{"chevron-down", 0xEA5F}, // chevron-down
{"caret-up", 0xFB2D}, // caret-up-filled
{"caret-down", 0xFB2A}, // caret-down-filled
{"caret-left", 0xFB2B}, // caret-left-filled
{"caret-right", 0xFB2C}, // caret-right-filled
// Wallpaper / color
{"camera-video", 0xED22}, // video
{"wallpaper-selector", 0xFD4A}, // library-photo
{"color-picker", 0xEBE6}, // color-picker
// Battery
{"battery", 0xEA34}, // battery
{"battery-1", 0xEA2F}, // battery-1
{"battery-2", 0xEA30}, // battery-2
{"battery-3", 0xEA31}, // battery-3
{"battery-4", 0xEA32}, // battery-4
{"battery-charging", 0xEA33}, // battery-charging
{"battery-charging-2", 0xEF3B}, // battery-charging-2
{"battery-exclamation", 0xFF1D}, // battery-exclamation
{"battery-off", 0xED1C}, // battery-off
// WiFi
{"wifi", 0xEB52}, // wifi
{"wifi-0", 0xEBA3}, // wifi-0
{"wifi-1", 0xEBA4}, // wifi-1
{"wifi-2", 0xEBA5}, // wifi-2
{"wifi-off", 0xECFA}, // wifi-off
// Bluetooth devices
{"bluetooth", 0xEA37}, // bluetooth
{"bt-device-generic", 0xEA37}, // bluetooth
{"bt-device-gamepad", 0xF1D2}, // device-gamepad-2
{"bt-device-microphone", 0xEAF0}, // microphone
{"bt-device-headset", 0xEB90}, // headset
{"bt-device-earbuds", 0xF5A9}, // device-airpods
{"bt-device-headphones", 0xEABD}, // headphones
{"bt-device-mouse", 0xF1D7}, // mouse-2
{"bt-device-keyboard", 0xEA37}, // bluetooth
{"bt-device-phone", 0xEA8A}, // device-mobile
{"bt-device-watch", 0xEBF9}, // device-watch
{"bt-device-speaker", 0xEA8B}, // device-speaker
{"bt-device-tv", 0xEA8D}, // device-tv
// Antenna
{"antenna-bars-1", 0xECC7}, // antenna-bars-1
{"antenna-bars-2", 0xECC8}, // antenna-bars-2
{"antenna-bars-3", 0xECC9}, // antenna-bars-3
{"antenna-bars-4", 0xECCA}, // antenna-bars-4
{"antenna-bars-5", 0xECCB}, // antenna-bars-5
{"antenna-bars-off", 0xF0AA}, // antenna-bars-off
// Weather
{"weather-sun", 0xEB30}, // sun
{"weather-moon", 0xEAF8}, // moon
{"weather-moon-stars", 0xECE7}, // moon-stars
{"weather-cloud", 0xEA76}, // cloud
{"weather-cloud-off", 0xED3E}, // cloud-off
{"weather-cloud-haze", 0xECD9}, // cloud-fog
{"weather-cloud-lightning", 0xF84B}, // cloud-bolt
{"weather-cloud-rain", 0xEA72}, // cloud-rain
{"weather-cloud-snow", 0xEA73}, // cloud-snow
{"weather-cloud-sun", 0xEC6D}, // cloud-sun
// Settings tabs
{"settings-general", 0xEC38}, // adjustments-horizontal
{"settings-bar", 0xFD51}, // crop-16-9
{"settings-user-interface", 0xEF95}, // layout-board
{"settings-control-center", 0xEC38}, // adjustments-horizontal
{"settings-dock", 0xEAD3}, // layout-bottombar
{"settings-launcher", 0xEC45}, // rocket
{"settings-audio", 0xEA8B}, // device-speaker
{"settings-display", 0xEA89}, // device-desktop
{"settings-network", 0xF4C3}, // circles-relation
{"settings-brightness", 0xEB7E}, // brightness-up
{"settings-location", 0xF9E4}, // world-pin
{"settings-color-scheme", 0xEB01}, // palette
{"settings-wallpaper", 0xEB00}, // paint
{"settings-wallpaper-selector", 0xFD4A}, // library-photo
{"settings-hooks", 0xEADE}, // link
{"settings-notifications", 0xEA35}, // bell
{"settings-osd", 0xED35}, // picture-in-picture
{"settings-about", 0xF635}, // info-square-rounded
{"settings-idle", 0xEAF8}, // moon
{"settings-lock-screen", 0xEAE2}, // lock
{"settings-session-menu", 0xEB0D}, // power
{"settings-system-monitor", 0xED23}, // activity
// Branding
{"noctalia", 0xEC33}, // noctalia
{"hyprland", 0xEC6A}, // hyprland
};
// clang-format on
} // namespace
char32_t IconRegistry::lookup(std::string_view name) {
auto it = kIcons.find(std::string(name));
if (it != kIcons.end()) {
return it->second;
}
return 0;
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <string_view>
// Hand-curated alias → codepoint map.
// To add a new icon, find its codepoint in assets/fonts/tabler-icons.json
// and add an entry here.
namespace IconRegistry {
[[nodiscard]] char32_t lookup(std::string_view name);
} // namespace IconRegistry
+15
View File
@@ -0,0 +1,15 @@
#include "ui/widgets/NotificationWidget.hpp"
#include "ui/controls/Icon.hpp"
void NotificationWidget::create(Renderer& renderer) {
auto icon = std::make_unique<Icon>();
icon->setIcon("bell");
m_icon = icon.get();
m_root = std::move(icon);
m_icon->measure(renderer);
}
void NotificationWidget::layout(Renderer& renderer, float /*barWidth*/, float /*barHeight*/) {
m_icon->measure(renderer);
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include "ui/Widget.hpp"
class Icon;
class NotificationWidget : public Widget {
public:
void create(Renderer& renderer) override;
void layout(Renderer& renderer, float barWidth, float barHeight) override;
private:
Icon* m_icon = nullptr;
};