ladybird/Userland/Libraries/LibGUI/ValueSlider.cpp
Andreas Oppebøen 0e4586dd3e LibGUI: Fix slightly wrong value slider knob position
When clicking the value slider bar, the knob was not positioned as
close to the mouse as expected. When moving the mouse right, the knob
would lag a bit behind, and the cursor would eventually be outside the
knob.

The bug was due to knob_rect() taking knob_thickness into account to
prevent the knob from protruding outside the bar. The value_at(pos)
calculated the relative position based on the entire width of the bar.
This discrepancy is fixed by taking knob_thickness into account in
value_at(position).
2023-03-24 10:33:22 +01:00

229 lines
6.9 KiB
C++

/*
* Copyright (c) 2021, Marcus Nilsson <brainbomb@gmail.com>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Painter.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/ValueSlider.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibGfx/Palette.h>
#include <LibGfx/StylePainter.h>
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_preferred_size(SpecialDimension::Fit);
m_textbox = add<GUI::TextBox>();
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 = [&]() {
DeprecatedString value = m_textbox->text();
if (value.ends_with(m_suffix, AK::CaseSensitivity::CaseInsensitive))
value = value.substring_view(0, value.length() - m_suffix.bytes_as_string_view().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::increase_slider_by(1);
m_textbox->set_text(formatted_value());
};
m_textbox->on_down_pressed = [&]() {
if (value() > min())
AbstractSlider::decrease_slider_by(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);
};
}
DeprecatedString ValueSlider::formatted_value() const
{
return DeprecatedString::formatted("{:2}{}", value(), m_suffix);
}
void ValueSlider::paint_event(PaintEvent& event)
{
GUI::Painter painter(*this);
painter.add_clip_rect(event.rect());
if (is_enabled())
painter.fill_rect_with_gradient(m_orientation, bar_rect(), palette().active_window_border1(), palette().active_window_border2());
else
painter.fill_rect_with_gradient(m_orientation, bar_rect(), palette().inactive_window_border1(), palette().inactive_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;
}
int ValueSlider::knob_length() const
{
return m_knob_style == KnobStyle::Wide ? 13 : 7;
}
Gfx::IntRect ValueSlider::knob_rect() const
{
int knob_thickness = knob_length();
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(Gfx::IntPoint position) const
{
int knob_thickness = knob_length();
float leftmost_knob_center = (float)bar_rect().left() + (float)knob_thickness / 2;
if (position.x() < leftmost_knob_center)
return min();
float rightmost_knob_center = (float)bar_rect().right() - (float)knob_thickness / 2;
if (position.x() > rightmost_knob_center)
return max();
float relative_offset = (float)(position.x() - leftmost_knob_center) / (rightmost_knob_center - leftmost_knob_center);
int range = max() - min();
return min() + (int)roundf(relative_offset * (float)range);
}
void ValueSlider::set_value(int value, AllowCallback allow_callback, DoClamp do_clamp)
{
AbstractSlider::set_value(value, allow_callback, do_clamp);
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_y() < 0)
increase_slider_by(1);
else
decrease_slider_by(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::Primary)
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::Primary)
return;
m_dragging = false;
}
Optional<UISize> ValueSlider::calculated_min_size() const
{
auto content_min_size = m_textbox->effective_min_size();
if (orientation() == Gfx::Orientation::Vertical)
return { { content_min_size.width(), content_min_size.height().as_int() + knob_length() } };
return { { content_min_size.width().as_int() + knob_length(), content_min_size.height() } };
}
Optional<UISize> ValueSlider::calculated_preferred_size() const
{
if (orientation() == Gfx::Orientation::Vertical)
return { { SpecialDimension::Shrink, SpecialDimension::OpportunisticGrow } };
return { { SpecialDimension::OpportunisticGrow, SpecialDimension::Shrink } };
}
}