Serenity Operating System
1/*
2 * Copyright (c) 2021, Marcus Nilsson <brainbomb@gmail.com>
3 * Copyright (c) 2022, the SerenityOS developers.
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <LibGUI/BoxLayout.h>
9#include <LibGUI/Painter.h>
10#include <LibGUI/TextBox.h>
11#include <LibGUI/ValueSlider.h>
12#include <LibGfx/Font/FontDatabase.h>
13#include <LibGfx/Palette.h>
14#include <LibGfx/StylePainter.h>
15
16REGISTER_WIDGET(GUI, ValueSlider)
17
18namespace GUI {
19
20ValueSlider::ValueSlider(Gfx::Orientation orientation, String suffix)
21 : AbstractSlider(orientation)
22 , m_suffix(move(suffix))
23{
24 // FIXME: Implement vertical mode
25 VERIFY(orientation == Orientation::Horizontal);
26
27 set_preferred_size(SpecialDimension::Fit);
28
29 m_textbox = add<GUI::TextBox>();
30 m_textbox->set_relative_rect({ 0, 0, 34, 20 });
31 m_textbox->set_font_fixed_width(true);
32 m_textbox->set_font_size(8);
33
34 m_textbox->on_change = [&]() {
35 DeprecatedString value = m_textbox->text();
36 if (value.ends_with(m_suffix, AK::CaseSensitivity::CaseInsensitive))
37 value = value.substring_view(0, value.length() - m_suffix.bytes_as_string_view().length());
38 auto integer_value = value.to_int();
39 if (integer_value.has_value())
40 AbstractSlider::set_value(integer_value.value());
41 };
42
43 m_textbox->on_return_pressed = [&]() {
44 m_textbox->on_change();
45 m_textbox->set_text(formatted_value());
46 };
47
48 m_textbox->on_up_pressed = [&]() {
49 if (value() < max())
50 AbstractSlider::increase_slider_by(1);
51 m_textbox->set_text(formatted_value());
52 };
53
54 m_textbox->on_down_pressed = [&]() {
55 if (value() > min())
56 AbstractSlider::decrease_slider_by(1);
57 m_textbox->set_text(formatted_value());
58 };
59
60 m_textbox->on_focusout = [&]() {
61 m_textbox->on_return_pressed();
62 };
63
64 m_textbox->on_escape_pressed = [&]() {
65 m_textbox->clear_selection();
66 m_textbox->set_text(formatted_value());
67 parent_widget()->set_focus(true);
68 };
69}
70
71DeprecatedString ValueSlider::formatted_value() const
72{
73 return DeprecatedString::formatted("{:2}{}", value(), m_suffix);
74}
75
76void ValueSlider::paint_event(PaintEvent& event)
77{
78 GUI::Painter painter(*this);
79 painter.add_clip_rect(event.rect());
80
81 if (is_enabled())
82 painter.fill_rect_with_gradient(m_orientation, bar_rect(), palette().active_window_border1(), palette().active_window_border2());
83 else
84 painter.fill_rect_with_gradient(m_orientation, bar_rect(), palette().inactive_window_border1(), palette().inactive_window_border2());
85
86 auto unfilled_rect = bar_rect();
87 unfilled_rect.set_left(knob_rect().right());
88 painter.fill_rect(unfilled_rect, palette().base());
89
90 Gfx::StylePainter::paint_frame(painter, bar_rect(), palette(), Gfx::FrameShape::Container, Gfx::FrameShadow::Sunken, 2);
91 Gfx::StylePainter::paint_button(painter, knob_rect(), palette(), Gfx::ButtonStyle::Normal, false, m_hovered);
92
93 auto paint_knurl = [&](int x, int y) {
94 painter.set_pixel(x, y, palette().threed_shadow1());
95 painter.set_pixel(x + 1, y, palette().threed_shadow1());
96 painter.set_pixel(x, y + 1, palette().threed_shadow1());
97 painter.set_pixel(x + 1, y + 1, palette().threed_highlight());
98 };
99
100 auto knurl_rect = knob_rect().shrunken(4, 8);
101
102 if (m_knob_style == KnobStyle::Wide) {
103 for (int i = 0; i < 4; ++i) {
104 paint_knurl(knurl_rect.x(), knurl_rect.y() + (i * 3));
105 paint_knurl(knurl_rect.x() + 3, knurl_rect.y() + (i * 3));
106 paint_knurl(knurl_rect.x() + 6, knurl_rect.y() + (i * 3));
107 }
108 } else {
109 for (int i = 0; i < 4; ++i)
110 paint_knurl(knurl_rect.x(), knurl_rect.y() + (i * 3));
111 }
112}
113
114Gfx::IntRect ValueSlider::bar_rect() const
115{
116 auto bar_rect = rect();
117 bar_rect.set_width(rect().width() - m_textbox->width());
118 bar_rect.set_x(m_textbox->width());
119 return bar_rect;
120}
121
122int ValueSlider::knob_length() const
123{
124 return m_knob_style == KnobStyle::Wide ? 13 : 7;
125}
126
127Gfx::IntRect ValueSlider::knob_rect() const
128{
129 int knob_thickness = knob_length();
130
131 Gfx::IntRect knob_rect = bar_rect();
132 knob_rect.set_width(knob_thickness);
133
134 int knob_offset = (int)((float)bar_rect().left() + (float)(value() - min()) / (float)(max() - min()) * (float)(bar_rect().width() - knob_thickness));
135 knob_rect.set_left(knob_offset);
136 knob_rect.center_vertically_within(bar_rect());
137 return knob_rect;
138}
139
140int ValueSlider::value_at(Gfx::IntPoint position) const
141{
142 if (position.x() < bar_rect().left())
143 return min();
144 if (position.x() > bar_rect().right())
145 return max();
146 float relative_offset = (float)(position.x() - bar_rect().left()) / (float)bar_rect().width();
147
148 int range = max() - min();
149 return min() + (int)(relative_offset * (float)range);
150}
151
152void ValueSlider::set_value(int value, AllowCallback allow_callback, DoClamp do_clamp)
153{
154 AbstractSlider::set_value(value, allow_callback, do_clamp);
155 m_textbox->set_text(formatted_value());
156}
157
158void ValueSlider::leave_event(Core::Event&)
159{
160 if (!m_hovered)
161 return;
162
163 m_hovered = false;
164 update(knob_rect());
165}
166
167void ValueSlider::mousewheel_event(MouseEvent& event)
168{
169 if (event.wheel_delta_y() < 0)
170 increase_slider_by(1);
171 else
172 decrease_slider_by(1);
173}
174
175void ValueSlider::mousemove_event(MouseEvent& event)
176{
177 bool is_hovered = knob_rect().contains(event.position());
178 if (is_hovered != m_hovered) {
179 m_hovered = is_hovered;
180 update(knob_rect());
181 }
182
183 if (!m_dragging)
184 return;
185
186 set_value(value_at(event.position()));
187}
188
189void ValueSlider::mousedown_event(MouseEvent& event)
190{
191 if (event.button() != MouseButton::Primary)
192 return;
193
194 m_textbox->set_focus(true);
195
196 if (bar_rect().contains(event.position())) {
197 m_dragging = true;
198 set_value(value_at(event.position()));
199 }
200}
201
202void ValueSlider::mouseup_event(MouseEvent& event)
203{
204 if (event.button() != MouseButton::Primary)
205 return;
206
207 m_dragging = false;
208}
209
210Optional<UISize> ValueSlider::calculated_min_size() const
211{
212 auto content_min_size = m_textbox->effective_min_size();
213
214 if (orientation() == Gfx::Orientation::Vertical)
215 return { { content_min_size.width(), content_min_size.height().as_int() + knob_length() } };
216 return { { content_min_size.width().as_int() + knob_length(), content_min_size.height() } };
217}
218
219Optional<UISize> ValueSlider::calculated_preferred_size() const
220{
221 if (orientation() == Gfx::Orientation::Vertical)
222 return { { SpecialDimension::Shrink, SpecialDimension::OpportunisticGrow } };
223 return { { SpecialDimension::OpportunisticGrow, SpecialDimension::Shrink } };
224}
225
226}