diff --git a/Userland/Libraries/LibGUI/AbstractSlider.h b/Userland/Libraries/LibGUI/AbstractSlider.h index 5ce5f79497..aec489c875 100644 --- a/Userland/Libraries/LibGUI/AbstractSlider.h +++ b/Userland/Libraries/LibGUI/AbstractSlider.h @@ -27,7 +27,7 @@ public: bool jump_to_cursor() const { return m_jump_to_cursor; } void set_range(int min, int max); - void set_value(int); + virtual void set_value(int); void set_min(int min) { set_range(min, max()); } void set_max(int max) { set_range(min(), max); } diff --git a/Userland/Libraries/LibGUI/CMakeLists.txt b/Userland/Libraries/LibGUI/CMakeLists.txt index 3b1a5735a1..84104f270e 100644 --- a/Userland/Libraries/LibGUI/CMakeLists.txt +++ b/Userland/Libraries/LibGUI/CMakeLists.txt @@ -97,6 +97,7 @@ set(SOURCES ToolbarContainer.cpp TreeView.cpp UndoStack.cpp + ValueSlider.cpp Variant.cpp VimEditingEngine.cpp Widget.cpp diff --git a/Userland/Libraries/LibGUI/Forward.h b/Userland/Libraries/LibGUI/Forward.h index 30d1c4d76f..a01589be6c 100644 --- a/Userland/Libraries/LibGUI/Forward.h +++ b/Userland/Libraries/LibGUI/Forward.h @@ -74,6 +74,7 @@ class FontsChangeEvent; class Toolbar; class ToolbarContainer; class TreeView; +class ValueSlider; class Variant; class VerticalBoxLayout; class VerticalSlider; diff --git a/Userland/Libraries/LibGUI/ValueSlider.cpp b/Userland/Libraries/LibGUI/ValueSlider.cpp new file mode 100644 index 0000000000..4039f884d6 --- /dev/null +++ b/Userland/Libraries/LibGUI/ValueSlider.cpp @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2021, Marcus Nilsson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +REGISTER_WIDGET(GUI, ValueSlider) + +namespace GUI { + +ValueSlider::ValueSlider(Gfx::Orientation orientation, String suffix) + : AbstractSlider(orientation) + , m_suffix(move(suffix)) +{ + //FIXME: Implement vertical mode + VERIFY(orientation == Orientation::Horizontal); + + set_fixed_height(20); + + m_textbox = add(); + m_textbox->set_relative_rect({ 0, 0, 34, 20 }); + m_textbox->set_font_fixed_width(true); + m_textbox->set_font_size(8); + + m_textbox->on_change = [&]() { + String value = m_textbox->text(); + if (value.ends_with(m_suffix, AK::CaseSensitivity::CaseInsensitive)) + value = value.substring_view(0, value.length() - m_suffix.length()); + auto integer_value = value.to_int(); + if (integer_value.has_value()) + AbstractSlider::set_value(integer_value.value()); + }; + + m_textbox->on_return_pressed = [&]() { + m_textbox->on_change(); + m_textbox->set_text(formatted_value()); + }; + + m_textbox->on_up_pressed = [&]() { + if (value() < max()) + AbstractSlider::set_value(value() + 1); + m_textbox->set_text(formatted_value()); + }; + + m_textbox->on_down_pressed = [&]() { + if (value() > min()) + AbstractSlider::set_value(value() - 1); + m_textbox->set_text(formatted_value()); + }; + + m_textbox->on_focusout = [&]() { + m_textbox->on_return_pressed(); + }; + + m_textbox->on_escape_pressed = [&]() { + m_textbox->clear_selection(); + m_textbox->set_text(formatted_value()); + parent_widget()->set_focus(true); + }; +} + +ValueSlider::~ValueSlider() +{ +} + +String ValueSlider::formatted_value() const +{ + return String::formatted("{:2}{}", value(), m_suffix); +} + +void ValueSlider::paint_event(PaintEvent& event) +{ + GUI::Painter painter(*this); + painter.add_clip_rect(event.rect()); + + painter.fill_rect_with_gradient(m_orientation, bar_rect(), palette().active_window_border1(), palette().active_window_border2()); + auto unfilled_rect = bar_rect(); + unfilled_rect.set_left(knob_rect().right()); + painter.fill_rect(unfilled_rect, palette().base()); + + Gfx::StylePainter::paint_frame(painter, bar_rect(), palette(), Gfx::FrameShape::Container, Gfx::FrameShadow::Sunken, 2); + Gfx::StylePainter::paint_button(painter, knob_rect(), palette(), Gfx::ButtonStyle::Normal, false, m_hovered); + + auto paint_knurl = [&](int x, int y) { + painter.set_pixel(x, y, palette().threed_shadow1()); + painter.set_pixel(x + 1, y, palette().threed_shadow1()); + painter.set_pixel(x, y + 1, palette().threed_shadow1()); + painter.set_pixel(x + 1, y + 1, palette().threed_highlight()); + }; + + auto knurl_rect = knob_rect().shrunken(4, 8); + + if (m_knob_style == KnobStyle::Wide) { + for (int i = 0; i < 4; ++i) { + paint_knurl(knurl_rect.x(), knurl_rect.y() + (i * 3)); + paint_knurl(knurl_rect.x() + 3, knurl_rect.y() + (i * 3)); + paint_knurl(knurl_rect.x() + 6, knurl_rect.y() + (i * 3)); + } + } else { + for (int i = 0; i < 4; ++i) + paint_knurl(knurl_rect.x(), knurl_rect.y() + (i * 3)); + } +} + +Gfx::IntRect ValueSlider::bar_rect() const +{ + auto bar_rect = rect(); + bar_rect.set_width(rect().width() - m_textbox->width()); + bar_rect.set_x(m_textbox->width()); + return bar_rect; +} + +Gfx::IntRect ValueSlider::knob_rect() const +{ + int knob_thickness = m_knob_style == KnobStyle::Wide ? 13 : 7; + + Gfx::IntRect knob_rect = bar_rect(); + knob_rect.set_width(knob_thickness); + + int knob_offset = (int)((float)bar_rect().left() + (float)(value() - min()) / (float)(max() - min()) * (float)(bar_rect().width() - knob_thickness)); + knob_rect.set_left(knob_offset); + knob_rect.center_vertically_within(bar_rect()); + return knob_rect; +} + +int ValueSlider::value_at(const Gfx::IntPoint& position) const +{ + if (position.x() < bar_rect().left()) + return min(); + if (position.x() > bar_rect().right()) + return max(); + float relative_offset = (float)(position.x() - bar_rect().left()) / (float)bar_rect().width(); + return (int)(relative_offset * (float)max()); +} + +void ValueSlider::set_value(int value) +{ + AbstractSlider::set_value(value); + m_textbox->set_text(formatted_value()); +} + +void ValueSlider::leave_event(Core::Event&) +{ + if (!m_hovered) + return; + + m_hovered = false; + update(knob_rect()); +} + +void ValueSlider::mousewheel_event(MouseEvent& event) +{ + if (event.wheel_delta() < 0) + set_value(value() + 1); + else + set_value(value() - 1); +} + +void ValueSlider::mousemove_event(MouseEvent& event) +{ + bool is_hovered = knob_rect().contains(event.position()); + if (is_hovered != m_hovered) { + m_hovered = is_hovered; + update(knob_rect()); + } + + if (!m_dragging) + return; + + set_value(value_at(event.position())); +} + +void ValueSlider::mousedown_event(MouseEvent& event) +{ + if (event.button() != MouseButton::Left) + return; + + m_textbox->set_focus(true); + + if (bar_rect().contains(event.position())) { + m_dragging = true; + set_value(value_at(event.position())); + } +} + +void ValueSlider::mouseup_event(MouseEvent& event) +{ + if (event.button() != MouseButton::Left) + return; + + m_dragging = false; +} + +} diff --git a/Userland/Libraries/LibGUI/ValueSlider.h b/Userland/Libraries/LibGUI/ValueSlider.h new file mode 100644 index 0000000000..886e1adf18 --- /dev/null +++ b/Userland/Libraries/LibGUI/ValueSlider.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021, Marcus Nilsson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace GUI { + +class ValueSlider : public AbstractSlider { + C_OBJECT(ValueSlider); + +public: + enum class KnobStyle { + Wide, + Thin, + }; + + virtual ~ValueSlider() override; + + void set_suffix(String suffix) { m_suffix = move(suffix); } + void set_knob_style(KnobStyle knobstyle) { m_knob_style = knobstyle; } + + virtual void set_value(int value) override; + +protected: + virtual void paint_event(PaintEvent&) override; + virtual void mousedown_event(MouseEvent&) override; + virtual void mousemove_event(MouseEvent&) override; + virtual void mouseup_event(MouseEvent&) override; + virtual void mousewheel_event(MouseEvent&) override; + virtual void leave_event(Core::Event&) override; + +private: + explicit ValueSlider(Gfx::Orientation = Gfx::Orientation::Horizontal, String suffix = ""); + + String formatted_value() const; + int value_at(const Gfx::IntPoint& position) const; + Gfx::IntRect bar_rect() const; + Gfx::IntRect knob_rect() const; + + String m_suffix {}; + Orientation m_orientation { Orientation::Horizontal }; + KnobStyle m_knob_style { KnobStyle::Thin }; + RefPtr m_textbox; + bool m_dragging { false }; + bool m_hovered { false }; +}; + +}