mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(controls): add Slider
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user