feat(ui): added spinner (aka busy indicator)

This commit is contained in:
Lemmy
2026-04-04 20:36:04 -04:00
parent 5ea4d29cc5
commit 6c44afa857
12 changed files with 318 additions and 2 deletions
+2
View File
@@ -207,6 +207,7 @@ add_executable(noctalia
src/render/programs/LinearGradientProgram.cpp
src/render/programs/MsdfTextProgram.cpp
src/render/programs/RoundedRectProgram.cpp
src/render/programs/SpinnerProgram.cpp
src/render/programs/WallpaperProgram.cpp
src/render/text/MsdfTextRenderer.cpp
src/render/RenderContext.cpp
@@ -226,6 +227,7 @@ add_executable(noctalia
src/ui/controls/Icon.cpp
src/ui/controls/Label.cpp
src/ui/controls/Slider.cpp
src/ui/controls/Spinner.cpp
src/ui/controls/Toggle.cpp
src/ui/icons/IconRegistry.cpp
src/render/scene/InputArea.cpp
+2 -2
View File
@@ -200,12 +200,12 @@ gdbus call --session --dest dev.noctalia.Debug --object-path /dev/noctalia/Debug
- [x] Button
- [x] IconButton
- [x] Slider
- [X] Select
- [x] Spinner
- [ ] Progress bar
- [ ] Busy Indicator
- [ ] Scroll view
- [ ] List view
- [ ] Text input
- [ ] Select
- [ ] Checkbox
- [ ] Radio button
- [ ] Tab bar
+10
View File
@@ -4,6 +4,7 @@
#include "render/scene/ImageNode.h"
#include "render/scene/Node.h"
#include "render/scene/RectNode.h"
#include "render/scene/SpinnerNode.h"
#include "render/scene/TextNode.h"
#include "ui/style/Style.h"
@@ -97,6 +98,7 @@ void RenderContext::ensureGlPrograms() {
m_imageProgram.ensureInitialized();
m_linearGradientProgram.ensureInitialized();
m_roundedRectProgram.ensureInitialized();
m_spinnerProgram.ensureInitialized();
m_glReady = true;
}
@@ -192,6 +194,13 @@ void RenderContext::renderNode(const Node* node, float parentX, float parentY, f
}
break;
}
case NodeType::Spinner: {
const auto* spinner = static_cast<const SpinnerNode*>(node);
auto style = spinner->style();
style.color.a *= effectiveOpacity;
m_spinnerProgram.draw(sw, sh, absX, absY, node->width(), node->height(), style);
break;
}
case NodeType::Base:
break;
}
@@ -220,6 +229,7 @@ void RenderContext::cleanup() {
m_imageProgram.destroy();
m_linearGradientProgram.destroy();
m_roundedRectProgram.destroy();
m_spinnerProgram.destroy();
m_textRenderer.cleanup();
m_iconTextRenderer.cleanup();
m_glReady = false;
+2
View File
@@ -6,6 +6,7 @@
#include "render/programs/ImageProgram.h"
#include "render/programs/LinearGradientProgram.h"
#include "render/programs/RoundedRectProgram.h"
#include "render/programs/SpinnerProgram.h"
#include "render/text/MsdfTextRenderer.h"
#include <EGL/egl.h>
@@ -50,6 +51,7 @@ private:
ImageProgram m_imageProgram;
LinearGradientProgram m_linearGradientProgram;
RoundedRectProgram m_roundedRectProgram;
SpinnerProgram m_spinnerProgram;
MsdfTextRenderer m_textRenderer;
MsdfTextRenderer m_iconTextRenderer;
TextureManager m_textureManager;
+126
View File
@@ -0,0 +1,126 @@
#include "render/programs/SpinnerProgram.h"
#include <array>
#include <stdexcept>
namespace {
constexpr char kVertexShaderSource[] = R"(
precision highp float;
attribute vec2 a_position;
uniform vec2 u_surface_size;
uniform vec4 u_quad_rect;
uniform vec4 u_rect;
varying vec2 v_pixel;
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_quad_rect.xy + (a_position * u_quad_rect.zw);
v_pixel = pixel_pos;
gl_Position = vec4(to_ndc(pixel_pos), 0.0, 1.0);
}
)";
constexpr char kFragmentShaderSource[] = R"(
precision highp float;
uniform vec4 u_rect;
uniform vec4 u_color;
uniform float u_thickness;
uniform float u_angle;
varying vec2 v_pixel;
const float PI = 3.14159265359;
void main() {
vec2 center = u_rect.xy + u_rect.zw * 0.5;
float radius = min(u_rect.z, u_rect.w) * 0.5 - u_thickness * 0.5;
vec2 p = v_pixel - center;
float dist = length(p);
// Ring SDF
float ring = abs(dist - radius) - u_thickness * 0.5;
float aa = 0.85;
float ringMask = 1.0 - smoothstep(-aa, aa, ring);
// Notch: hide a 90-degree arc centered at the current angle
float theta = atan(p.y, p.x);
float diff = mod(theta - u_angle + 3.0 * PI, 2.0 * PI) - PI;
float notchHalf = PI * 0.25;
float notchMask = smoothstep(-0.08, 0.08, abs(diff) - notchHalf);
float alpha = ringMask * notchMask * u_color.a;
if (alpha <= 0.0) {
discard;
}
gl_FragColor = vec4(u_color.rgb * alpha, alpha);
}
)";
} // namespace
void SpinnerProgram::ensureInitialized() {
if (m_program.isValid()) {
return;
}
m_program.create(kVertexShaderSource, kFragmentShaderSource);
m_positionLocation = glGetAttribLocation(m_program.id(), "a_position");
m_surfaceSizeLocation = glGetUniformLocation(m_program.id(), "u_surface_size");
m_quadRectLocation = glGetUniformLocation(m_program.id(), "u_quad_rect");
m_rectLocation = glGetUniformLocation(m_program.id(), "u_rect");
m_colorLocation = glGetUniformLocation(m_program.id(), "u_color");
m_thicknessLocation = glGetUniformLocation(m_program.id(), "u_thickness");
m_angleLocation = glGetUniformLocation(m_program.id(), "u_angle");
if (m_positionLocation < 0 || m_surfaceSizeLocation < 0 || m_quadRectLocation < 0 || m_rectLocation < 0 ||
m_colorLocation < 0 || m_thicknessLocation < 0 || m_angleLocation < 0) {
throw std::runtime_error("failed to query spinner shader locations");
}
}
void SpinnerProgram::destroy() {
m_program.destroy();
m_positionLocation = -1;
m_surfaceSizeLocation = -1;
m_quadRectLocation = -1;
m_rectLocation = -1;
m_colorLocation = -1;
m_thicknessLocation = -1;
m_angleLocation = -1;
}
void SpinnerProgram::draw(float surfaceWidth, float surfaceHeight, float x, float y, float width, float height,
const SpinnerStyle& style) const {
if (!m_program.isValid() || width <= 0.0f || height <= 0.0f) {
return;
}
const std::array<GLfloat, 12> vertices = {
0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
};
const float padding = style.thickness + 2.0f;
const float quadX = x - padding;
const float quadY = y - padding;
const float quadWidth = width + padding * 2.0f;
const float quadHeight = height + padding * 2.0f;
glUseProgram(m_program.id());
glUniform2f(m_surfaceSizeLocation, surfaceWidth, surfaceHeight);
glUniform4f(m_quadRectLocation, quadX, quadY, quadWidth, quadHeight);
glUniform4f(m_rectLocation, x, y, width, height);
glUniform4f(m_colorLocation, style.color.r, style.color.g, style.color.b, style.color.a);
glUniform1f(m_thicknessLocation, style.thickness);
glUniform1f(m_angleLocation, style.angle);
glVertexAttribPointer(m_positionLocation, 2, GL_FLOAT, GL_FALSE, 0, vertices.data());
glEnableVertexAttribArray(m_positionLocation);
glDrawArrays(GL_TRIANGLES, 0, 6);
glDisableVertexAttribArray(m_positionLocation);
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "render/core/Color.h"
#include "render/core/ShaderProgram.h"
#include <GLES2/gl2.h>
struct SpinnerStyle {
Color color{};
float thickness = 2.0f;
float angle = 0.0f;
};
class SpinnerProgram {
public:
SpinnerProgram() = default;
~SpinnerProgram() = default;
SpinnerProgram(const SpinnerProgram&) = delete;
SpinnerProgram& operator=(const SpinnerProgram&) = delete;
void ensureInitialized();
void destroy();
void draw(float surfaceWidth, float surfaceHeight, float x, float y, float width, float height,
const SpinnerStyle& style) const;
private:
ShaderProgram m_program;
GLint m_positionLocation = -1;
GLint m_surfaceSizeLocation = -1;
GLint m_quadRectLocation = -1;
GLint m_rectLocation = -1;
GLint m_colorLocation = -1;
GLint m_thicknessLocation = -1;
GLint m_angleLocation = -1;
};
+1
View File
@@ -10,6 +10,7 @@ enum class NodeType : std::uint8_t {
Text,
Image,
Icon,
Spinner,
};
class Node {
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "render/core/Color.h"
#include "render/programs/SpinnerProgram.h"
#include "render/scene/Node.h"
class SpinnerNode : public Node {
public:
SpinnerNode() : Node(NodeType::Spinner) {}
void setColor(const Color& color) { m_style.color = color; }
void setThickness(float thickness) { m_style.thickness = thickness; }
void setAngle(float angle) { m_style.angle = angle; }
[[nodiscard]] const Color& color() const noexcept { return m_style.color; }
[[nodiscard]] float thickness() const noexcept { return m_style.thickness; }
[[nodiscard]] float angle() const noexcept { return m_style.angle; }
[[nodiscard]] const SpinnerStyle& style() const noexcept { return m_style; }
private:
SpinnerStyle m_style;
};
+12
View File
@@ -6,6 +6,7 @@
#include "ui/controls/Select.h"
#include "ui/controls/Label.h"
#include "ui/controls/Slider.h"
#include "ui/controls/Spinner.h"
#include "ui/controls/Toggle.h"
#include "ui/style/Palette.h"
#include "ui/style/Style.h"
@@ -129,6 +130,17 @@ void TestPanelContent::create(Renderer& renderer) {
container->addChild(std::move(row));
}
{
auto spinner = std::make_unique<Spinner>();
spinner->setAnimationManager(m_animations);
spinner->start();
m_spinner = spinner.get();
auto row = makeRow();
row->addChild(makeRowLabel("Spinner", kRowLabelWidth));
row->addChild(std::move(spinner));
container->addChild(std::move(row));
}
m_root = std::move(container);
if (m_headerLabel != nullptr) {
+2
View File
@@ -7,6 +7,7 @@ class Button;
class Select;
class Label;
class Slider;
class Spinner;
class Toggle;
class TestPanelContent : public PanelContent {
@@ -27,4 +28,5 @@ private:
Button* m_iconButton = nullptr;
Slider* m_slider = nullptr;
Toggle* m_toggle = nullptr;
Spinner* m_spinner = nullptr;
};
+72
View File
@@ -0,0 +1,72 @@
#include "ui/controls/Spinner.h"
#include "render/scene/SpinnerNode.h"
#include "ui/style/Palette.h"
#include "ui/style/Style.h"
#include <cmath>
#include <memory>
namespace {
constexpr float kDefaultSize = 20.0f;
constexpr float kDefaultThickness = 2.0f;
constexpr float kRevolutionMs = 800.0f;
constexpr float kTwoPi = 2.0f * 3.14159265358979f;
} // namespace
Spinner::Spinner() {
auto node = std::make_unique<SpinnerNode>();
node->setColor(palette.primary);
node->setThickness(kDefaultThickness);
m_spinnerNode = static_cast<SpinnerNode*>(addChild(std::move(node)));
setSize(kDefaultSize, Style::controlHeight);
m_spinnerNode->setSize(kDefaultSize, kDefaultSize);
m_spinnerNode->setPosition(0.0f, (Style::controlHeight - kDefaultSize) * 0.5f);
}
void Spinner::setColor(const Color& color) { m_spinnerNode->setColor(color); }
void Spinner::setSpinnerSize(float size) {
setSize(size, size);
m_spinnerNode->setSize(size, size);
}
void Spinner::setThickness(float thickness) { m_spinnerNode->setThickness(thickness); }
void Spinner::start() {
if (m_spinning) {
return;
}
m_spinning = true;
startLoop();
}
void Spinner::stop() {
m_spinning = false;
if (m_animations != nullptr && m_animId != 0) {
m_animations->cancel(m_animId);
m_animId = 0;
}
}
void Spinner::startLoop() {
if (m_animations == nullptr || !m_spinning) {
return;
}
m_animId = m_animations->animate(
0.0f, kTwoPi, kRevolutionMs, Easing::Linear,
[this](float angle) {
m_spinnerNode->setAngle(angle);
markDirty();
},
[this]() {
m_animId = 0;
if (m_spinning) {
startLoop();
}
});
markDirty();
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include "render/animation/AnimationManager.h"
#include "render/core/Color.h"
#include "render/scene/Node.h"
class SpinnerNode;
class Spinner : public Node {
public:
Spinner();
void setColor(const Color& color);
void setSpinnerSize(float size);
void setThickness(float thickness);
void setAnimationManager(AnimationManager* mgr) noexcept { m_animations = mgr; }
void start();
void stop();
[[nodiscard]] bool spinning() const noexcept { return m_spinning; }
private:
void startLoop();
SpinnerNode* m_spinnerNode = nullptr;
AnimationManager* m_animations = nullptr;
AnimationManager::Id m_animId = 0;
bool m_spinning = false;
};