feat(controls): add Slider

This commit is contained in:
Lysec
2026-04-04 18:10:14 +02:00
parent 24b7bebc57
commit b00b62fa4a
8 changed files with 353 additions and 21 deletions
+1
View File
@@ -216,6 +216,7 @@ add_executable(noctalia
src/ui/controls/Icon.cpp
src/ui/controls/IconButton.cpp
src/ui/controls/Label.cpp
src/ui/controls/Slider.cpp
src/ui/controls/Toggle.cpp
src/ui/icons/IconRegistry.cpp
src/render/scene/InputArea.cpp
+1 -1
View File
@@ -160,7 +160,7 @@ gdbus call --session --dest dev.noctalia.Debug --object-path /dev/noctalia/Debug
- [x] Toggle
- [x] Button
- [x] IconButton
- [ ] Slider
- [x] Slider
- [ ] Tooltip
- [ ] Progress bar
- [ ] Scroll view
+86 -15
View File
@@ -5,24 +5,35 @@
#include "ui/controls/Button.h"
#include "ui/controls/IconButton.h"
#include "ui/controls/Label.h"
#include "ui/controls/Slider.h"
#include "ui/controls/Toggle.h"
#include "ui/style/Palette.h"
#include "ui/style/Style.h"
#include <cmath>
#include <memory>
#include <string>
void TestPanelContent::create(Renderer& renderer) {
auto container = std::make_unique<Box>();
container->setDirection(BoxDirection::Horizontal);
container->setDirection(BoxDirection::Vertical);
container->setGap(Style::spaceMd);
container->setAlign(BoxAlign::Center);
container->setAlign(BoxAlign::Start);
auto label = std::make_unique<Label>();
label->setText("Test Controls");
label->setFontSize(Style::fontSizeSm);
label->setColor(palette.onSurface);
m_label = label.get();
container->addChild(std::move(label));
auto header = std::make_unique<Label>();
header->setText("Test Controls");
header->setFontSize(Style::fontSizeSm);
header->setColor(palette.onSurface);
m_headerLabel = header.get();
container->addChild(std::move(header));
auto makeRow = []() {
auto row = std::make_unique<Box>();
row->setDirection(BoxDirection::Horizontal);
row->setGap(Style::spaceMd);
row->setAlign(BoxAlign::Center);
return row;
};
auto button = std::make_unique<Button>();
button->setText("Button");
@@ -33,7 +44,15 @@ void TestPanelContent::create(Renderer& renderer) {
}
});
m_button = button.get();
container->addChild(std::move(button));
{
auto row = makeRow();
auto rowLabel = std::make_unique<Label>();
rowLabel->setText("Button");
rowLabel->setCaptionStyle();
row->addChild(std::move(rowLabel));
row->addChild(std::move(button));
container->addChild(std::move(row));
}
auto iconButton = std::make_unique<IconButton>();
iconButton->setText("Settings");
@@ -45,7 +64,44 @@ void TestPanelContent::create(Renderer& renderer) {
}
});
m_iconButton = iconButton.get();
container->addChild(std::move(iconButton));
{
auto row = makeRow();
auto rowLabel = std::make_unique<Label>();
rowLabel->setText("IconButton");
rowLabel->setCaptionStyle();
row->addChild(std::move(rowLabel));
row->addChild(std::move(iconButton));
container->addChild(std::move(row));
}
auto slider = std::make_unique<Slider>();
slider->setRange(0.0f, 100.0f);
slider->setStep(1.0f);
slider->setValue(50.0f);
slider->setSize(180.0f, 0.0f);
slider->setOnValueChanged([this](float value) {
if (m_sliderValueLabel != nullptr) {
const int percent = static_cast<int>(std::round(value));
m_sliderValueLabel->setText(std::to_string(percent) + "%");
}
});
m_slider = slider.get();
{
auto row = makeRow();
auto rowLabel = std::make_unique<Label>();
rowLabel->setText("Slider");
rowLabel->setCaptionStyle();
row->addChild(std::move(rowLabel));
row->addChild(std::move(slider));
auto valueLabel = std::make_unique<Label>();
valueLabel->setText("50%");
valueLabel->setCaptionStyle();
m_sliderValueLabel = valueLabel.get();
row->addChild(std::move(valueLabel));
container->addChild(std::move(row));
}
auto area = std::make_unique<InputArea>();
area->setOnClick([this](const InputArea::PointerData& /*data*/) {
@@ -60,20 +116,32 @@ void TestPanelContent::create(Renderer& renderer) {
area->addChild(std::move(toggle));
m_container = container.get();
container->addChild(std::move(area));
{
auto row = makeRow();
auto rowLabel = std::make_unique<Label>();
rowLabel->setText("Toggle");
rowLabel->setCaptionStyle();
row->addChild(std::move(rowLabel));
row->addChild(std::move(area));
container->addChild(std::move(row));
}
m_root = std::move(container);
// Measure label so Box::layout can compute sizes
m_label->measure(renderer);
if (m_headerLabel != nullptr) {
m_headerLabel->measure(renderer);
}
}
void TestPanelContent::layout(Renderer& renderer, float /*width*/, float /*height*/) {
if (m_container == nullptr) {
return;
}
if (m_label != nullptr) {
m_label->measure(renderer);
if (m_headerLabel != nullptr) {
m_headerLabel->measure(renderer);
}
if (m_sliderValueLabel != nullptr) {
m_sliderValueLabel->measure(renderer);
}
if (m_button != nullptr) {
m_button->layout(renderer);
@@ -82,6 +150,9 @@ void TestPanelContent::layout(Renderer& renderer, float /*width*/, float /*heigh
if (m_iconButton != nullptr) {
m_iconButton->layout(renderer);
}
if (m_slider != nullptr) {
m_slider->layout(renderer);
}
if (m_toggle != nullptr) {
m_toggle->layout(renderer);
}
+4 -1
View File
@@ -6,6 +6,7 @@ class Box;
class Button;
class IconButton;
class Label;
class Slider;
class Toggle;
class TestPanelContent : public PanelContent {
@@ -19,8 +20,10 @@ public:
private:
Box* m_container = nullptr;
Label* m_label = nullptr;
Label* m_headerLabel = nullptr;
Label* m_sliderValueLabel = nullptr;
Button* m_button = nullptr;
IconButton* m_iconButton = nullptr;
Slider* m_slider = nullptr;
Toggle* m_toggle = nullptr;
};
+194
View File
@@ -0,0 +1,194 @@
#include "ui/controls/Slider.h"
#include "render/programs/RoundedRectProgram.h"
#include "render/scene/InputArea.h"
#include "render/scene/RectNode.h"
#include "ui/style/Palette.h"
#include "ui/style/Style.h"
#include "cursor-shape-v1-client-protocol.h"
#include <algorithm>
#include <cmath>
#include <memory>
namespace {
constexpr float kDefaultWidth = 180.0f;
constexpr float kTrackHeight = 4.0f;
constexpr float kThumbSize = 14.0f;
constexpr float kHorizontalPadding = 2.0f;
RoundedRectStyle solidStyle(const Color& fill, float radius) {
return RoundedRectStyle{
.fill = fill,
.border = fill,
.fillMode = FillMode::Solid,
.radius = radius,
.softness = 1.0f,
.borderWidth = 0.0f,
};
}
} // namespace
Slider::Slider() {
auto track = std::make_unique<RectNode>();
m_track = static_cast<RectNode*>(addChild(std::move(track)));
auto fill = std::make_unique<RectNode>();
m_fill = static_cast<RectNode*>(addChild(std::move(fill)));
auto thumb = std::make_unique<RectNode>();
m_thumb = static_cast<RectNode*>(addChild(std::move(thumb)));
auto area = std::make_unique<InputArea>();
area->setOnPress([this](const InputArea::PointerData& data) {
if (!m_enabled || data.button != 0x110 || !data.pressed) {
return;
}
updateFromLocalX(data.localX);
});
area->setOnMotion([this](const InputArea::PointerData& data) {
if (!m_enabled || m_inputArea == nullptr || !m_inputArea->pressed()) {
return;
}
updateFromLocalX(data.localX);
});
m_inputArea = static_cast<InputArea*>(addChild(std::move(area)));
m_inputArea->setCursorShape(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER);
applyVisualState();
}
void Slider::setRange(float minValue, float maxValue) {
if (maxValue < minValue) {
std::swap(minValue, maxValue);
}
if (m_min == minValue && m_max == maxValue) {
return;
}
m_min = minValue;
m_max = maxValue;
setValue(m_value);
}
void Slider::setStep(float step) {
m_step = std::max(step, 0.0f);
setValue(m_value);
}
void Slider::setValue(float value) {
const float next = snapped(value);
if (std::abs(next - m_value) < 0.0001f) {
return;
}
m_value = next;
updateGeometry();
markDirty();
if (m_onValueChanged) {
m_onValueChanged(m_value);
}
}
void Slider::setEnabled(bool enabled) {
if (m_enabled == enabled) {
return;
}
m_enabled = enabled;
applyVisualState();
markDirty();
}
void Slider::setOnValueChanged(std::function<void(float)> callback) { m_onValueChanged = std::move(callback); }
void Slider::layout(Renderer& /*renderer*/) {
updateGeometry();
applyVisualState();
}
void Slider::updateGeometry() {
const float widthPx = width() > 0.0f ? width() : kDefaultWidth;
const float heightPx = std::max(kThumbSize, kTrackHeight) + Style::spaceXs;
setSize(widthPx, heightPx);
const float trackY = (heightPx - kTrackHeight) * 0.5f;
const float trackX = kHorizontalPadding;
const float trackW = std::max(0.0f, widthPx - kHorizontalPadding * 2.0f);
const float t = normalizedValue();
const float thumbX = trackX + t * trackW;
const float thumbY = (heightPx - kThumbSize) * 0.5f;
m_track->setPosition(trackX, trackY);
m_track->setSize(trackW, kTrackHeight);
m_fill->setPosition(trackX, trackY);
m_fill->setSize(std::max(0.0f, thumbX - trackX), kTrackHeight);
m_thumb->setPosition(std::clamp(thumbX - kThumbSize * 0.5f, trackX, trackX + trackW - kThumbSize), thumbY);
m_thumb->setSize(kThumbSize, kThumbSize);
m_inputArea->setPosition(0.0f, 0.0f);
m_inputArea->setSize(widthPx, heightPx);
}
void Slider::updateFromLocalX(float x) {
const float widthPx = width() > 0.0f ? width() : kDefaultWidth;
const float trackX = kHorizontalPadding;
const float trackW = std::max(0.0f, widthPx - kHorizontalPadding * 2.0f);
if (trackW <= 0.0f) {
return;
}
const float t = std::clamp((x - trackX) / trackW, 0.0f, 1.0f);
setValue(m_min + t * (m_max - m_min));
}
void Slider::applyVisualState() {
const bool hovering = m_inputArea != nullptr && m_inputArea->hovered();
const bool pressing = m_inputArea != nullptr && m_inputArea->pressed();
Color trackColor = palette.outline;
Color fillColor = palette.primary;
Color thumbColor = palette.surface;
Color thumbBorder = palette.primary;
if (!m_enabled) {
trackColor = rgba(palette.outline.r, palette.outline.g, palette.outline.b, 0.5f);
fillColor = rgba(palette.primary.r, palette.primary.g, palette.primary.b, 0.5f);
thumbColor = rgba(palette.surface.r, palette.surface.g, palette.surface.b, 0.7f);
thumbBorder = rgba(palette.primary.r, palette.primary.g, palette.primary.b, 0.6f);
} else if (pressing) {
thumbColor = palette.primary;
thumbBorder = palette.primary;
} else if (hovering) {
thumbBorder = palette.secondary;
}
auto trackStyle = solidStyle(trackColor, kTrackHeight * 0.5f);
m_track->setStyle(trackStyle);
auto fillStyle = solidStyle(fillColor, kTrackHeight * 0.5f);
m_fill->setStyle(fillStyle);
auto thumbStyle = solidStyle(thumbColor, kThumbSize * 0.5f);
thumbStyle.border = thumbBorder;
thumbStyle.borderWidth = Style::borderWidth;
m_thumb->setStyle(thumbStyle);
}
float Slider::normalizedValue() const noexcept {
if (m_max <= m_min) {
return 0.0f;
}
return std::clamp((m_value - m_min) / (m_max - m_min), 0.0f, 1.0f);
}
float Slider::snapped(float value) const noexcept {
const float clamped = std::clamp(value, m_min, m_max);
if (m_step <= 0.0f || m_max <= m_min) {
return clamped;
}
const float steps = std::round((clamped - m_min) / m_step);
return std::clamp(m_min + steps * m_step, m_min, m_max);
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "ui/controls/Box.h"
#include <functional>
class InputArea;
class RectNode;
class Renderer;
class Slider : public Box {
public:
Slider();
void setRange(float minValue, float maxValue);
void setStep(float step);
void setValue(float value);
void setEnabled(bool enabled);
void setOnValueChanged(std::function<void(float)> callback);
[[nodiscard]] float value() const noexcept { return m_value; }
[[nodiscard]] float minValue() const noexcept { return m_min; }
[[nodiscard]] float maxValue() const noexcept { return m_max; }
[[nodiscard]] bool enabled() const noexcept { return m_enabled; }
void layout(Renderer& renderer) override;
private:
void updateFromLocalX(float x);
void updateGeometry();
void applyVisualState();
[[nodiscard]] float normalizedValue() const noexcept;
[[nodiscard]] float snapped(float value) const noexcept;
RectNode* m_track = nullptr;
RectNode* m_fill = nullptr;
RectNode* m_thumb = nullptr;
InputArea* m_inputArea = nullptr;
std::function<void(float)> m_onValueChanged;
float m_min = 0.0f;
float m_max = 100.0f;
float m_step = 1.0f;
float m_value = 50.0f;
bool m_enabled = true;
};
+16 -4
View File
@@ -85,17 +85,23 @@ void WaylandSeat::handleSeatName(void* /*data*/, wl_seat* /*seat*/, const char*
void WaylandSeat::handlePointerEnter(void* data, wl_pointer* /*pointer*/, std::uint32_t serial, wl_surface* surface,
std::int32_t sx, std::int32_t sy) {
auto* self = static_cast<WaylandSeat*>(data);
self->m_lastPointerSurface = surface;
self->m_lastPointerX = wl_fixed_to_double(sx);
self->m_lastPointerY = wl_fixed_to_double(sy);
self->m_hasPointerPosition = true;
self->m_pendingPointerEvents.push_back(PointerEvent{
.type = PointerEvent::Type::Enter,
.serial = serial,
.surface = surface,
.sx = wl_fixed_to_double(sx),
.sy = wl_fixed_to_double(sy),
.sx = self->m_lastPointerX,
.sy = self->m_lastPointerY,
});
}
void WaylandSeat::handlePointerLeave(void* data, wl_pointer* /*pointer*/, std::uint32_t serial, wl_surface* surface) {
auto* self = static_cast<WaylandSeat*>(data);
self->m_lastPointerSurface = surface;
self->m_hasPointerPosition = false;
self->m_pendingPointerEvents.push_back(PointerEvent{
.type = PointerEvent::Type::Leave,
.serial = serial,
@@ -106,10 +112,13 @@ void WaylandSeat::handlePointerLeave(void* data, wl_pointer* /*pointer*/, std::u
void WaylandSeat::handlePointerMotion(void* data, wl_pointer* /*pointer*/, std::uint32_t time, std::int32_t sx,
std::int32_t sy) {
auto* self = static_cast<WaylandSeat*>(data);
self->m_lastPointerX = wl_fixed_to_double(sx);
self->m_lastPointerY = wl_fixed_to_double(sy);
self->m_hasPointerPosition = true;
self->m_pendingPointerEvents.push_back(PointerEvent{
.type = PointerEvent::Type::Motion,
.sx = wl_fixed_to_double(sx),
.sy = wl_fixed_to_double(sy),
.sx = self->m_lastPointerX,
.sy = self->m_lastPointerY,
.time = time,
});
}
@@ -120,6 +129,9 @@ void WaylandSeat::handlePointerButton(void* data, wl_pointer* /*pointer*/, std::
self->m_pendingPointerEvents.push_back(PointerEvent{
.type = PointerEvent::Type::Button,
.serial = serial,
.surface = self->m_lastPointerSurface,
.sx = self->m_hasPointerPosition ? self->m_lastPointerX : 0.0,
.sy = self->m_hasPointerPosition ? self->m_lastPointerY : 0.0,
.time = time,
.button = button,
.state = state,
+4
View File
@@ -50,4 +50,8 @@ private:
wp_cursor_shape_device_v1* m_cursorShapeDevice = nullptr;
PointerEventCallback m_pointerEventCallback;
std::vector<PointerEvent> m_pendingPointerEvents;
wl_surface* m_lastPointerSurface = nullptr;
double m_lastPointerX = 0.0;
double m_lastPointerY = 0.0;
bool m_hasPointerPosition = false;
};