mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(ui): added spinner (aka busy indicator)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -10,6 +10,7 @@ enum class NodeType : std::uint8_t {
|
||||
Text,
|
||||
Image,
|
||||
Icon,
|
||||
Spinner,
|
||||
};
|
||||
|
||||
class Node {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user