Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2022, the SerenityOS developers.
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <AK/StdLibExtras.h>
9#include <LibGUI/Painter.h>
10#include <LibGUI/Slider.h>
11#include <LibGfx/Palette.h>
12#include <LibGfx/StylePainter.h>
13
14REGISTER_WIDGET(GUI, HorizontalSlider)
15REGISTER_WIDGET(GUI, Slider)
16REGISTER_WIDGET(GUI, VerticalSlider)
17
18namespace GUI {
19
20Slider::Slider(Orientation orientation)
21 : AbstractSlider(orientation)
22{
23 REGISTER_ENUM_PROPERTY("knob_size_mode", knob_size_mode, set_knob_size_mode, KnobSizeMode,
24 { KnobSizeMode::Fixed, "Fixed" },
25 { KnobSizeMode::Proportional, "Proportional" });
26 REGISTER_BOOL_PROPERTY("jump_to_cursor", jump_to_cursor, set_jump_to_cursor);
27
28 set_preferred_size(SpecialDimension::Fit);
29}
30
31void Slider::paint_event(PaintEvent& event)
32{
33 Painter painter(*this);
34 painter.add_clip_rect(event.rect());
35
36 Gfx::IntRect track_rect;
37
38 if (orientation() == Orientation::Horizontal) {
39 track_rect = { inner_rect().x(), 0, inner_rect().width(), track_size() };
40 track_rect.center_vertically_within(inner_rect());
41 } else {
42 track_rect = { 0, inner_rect().y(), track_size(), inner_rect().height() };
43 track_rect.center_horizontally_within(inner_rect());
44 }
45 Gfx::StylePainter::paint_frame(painter, track_rect, palette(), Gfx::FrameShape::Panel, Gfx::FrameShadow::Sunken, 1);
46 if (is_enabled())
47 Gfx::StylePainter::paint_button(painter, knob_rect(), palette(), Gfx::ButtonStyle::Normal, false, m_knob_hovered);
48 else
49 Gfx::StylePainter::paint_button(painter, knob_rect(), palette(), Gfx::ButtonStyle::Normal, true, m_knob_hovered);
50}
51
52Gfx::IntRect Slider::knob_rect() const
53{
54 auto inner_rect = this->inner_rect();
55 Gfx::IntRect rect;
56 rect.set_secondary_offset_for_orientation(orientation(), 0);
57 rect.set_secondary_size_for_orientation(orientation(), knob_secondary_size());
58
59 if (knob_size_mode() == KnobSizeMode::Fixed) {
60 if (max() - min()) {
61 float scale = (float)inner_rect.primary_size_for_orientation(orientation()) / (float)(max() - min());
62 rect.set_primary_offset_for_orientation(orientation(), inner_rect.primary_offset_for_orientation(orientation()) + ((int)((value() - min()) * scale)) - (knob_fixed_primary_size() / 2));
63 } else
64 rect.set_primary_size_for_orientation(orientation(), 0);
65 rect.set_primary_size_for_orientation(orientation(), knob_fixed_primary_size());
66 } else {
67 float scale = (float)inner_rect.primary_size_for_orientation(orientation()) / (float)(max() - min() + 1);
68 rect.set_primary_offset_for_orientation(orientation(), inner_rect.primary_offset_for_orientation(orientation()) + ((int)((value() - min()) * scale)));
69 if (max() - min())
70 rect.set_primary_size_for_orientation(orientation(), ::max((int)(scale), knob_fixed_primary_size()));
71 else
72 rect.set_primary_size_for_orientation(orientation(), inner_rect.primary_size_for_orientation(orientation()));
73 }
74 if (orientation() == Orientation::Horizontal)
75 rect.center_vertically_within(inner_rect);
76 else
77 rect.center_horizontally_within(inner_rect);
78 return rect;
79}
80
81void Slider::start_drag(Gfx::IntPoint start_position)
82{
83 VERIFY(!m_dragging);
84 m_dragging = true;
85 m_drag_origin = start_position;
86 m_drag_origin_value = value();
87 if (on_drag_start)
88 on_drag_start();
89}
90
91void Slider::mousedown_event(MouseEvent& event)
92{
93 if (event.button() == MouseButton::Primary) {
94 auto const mouse_offset = event.position().primary_offset_for_orientation(orientation());
95
96 if (jump_to_cursor()) {
97 float normalized_mouse_offset = 0.0f;
98 if (orientation() == Orientation::Vertical) {
99 normalized_mouse_offset = static_cast<float>(mouse_offset - track_margin()) / static_cast<float>(inner_rect().height());
100 } else {
101 normalized_mouse_offset = static_cast<float>(mouse_offset - track_margin()) / static_cast<float>(inner_rect().width());
102 }
103
104 int new_value = static_cast<int>(min() + ((max() - min()) * normalized_mouse_offset));
105 set_value(new_value, AllowCallback::No);
106 start_drag(event.position());
107 // Delay the callback to make it aware that a drag has started.
108 if (on_change)
109 on_change(value());
110 return;
111 }
112
113 if (knob_rect().contains(event.position())) {
114 start_drag(event.position());
115 return;
116 }
117
118 auto knob_first_edge = knob_rect().first_edge_for_orientation(orientation());
119 auto knob_last_edge = knob_rect().last_edge_for_orientation(orientation());
120 if (mouse_offset > knob_last_edge)
121 increase_slider_by_page_steps(1);
122 else if (mouse_offset < knob_first_edge)
123 decrease_slider_by_page_steps(1);
124 }
125 return Widget::mousedown_event(event);
126}
127
128void Slider::mousemove_event(MouseEvent& event)
129{
130 set_knob_hovered(knob_rect().contains(event.position()));
131 if (m_dragging) {
132 float delta = event.position().primary_offset_for_orientation(orientation()) - m_drag_origin.primary_offset_for_orientation(orientation());
133 float scrubbable_range = inner_rect().primary_size_for_orientation(orientation());
134 float value_steps_per_scrubbed_pixel = (max() - min()) / scrubbable_range;
135 float new_value = m_drag_origin_value + (value_steps_per_scrubbed_pixel * delta);
136 set_value((int)new_value);
137 return;
138 }
139 return Widget::mousemove_event(event);
140}
141
142void Slider::end_drag()
143{
144 if (m_dragging) {
145 m_dragging = false;
146 if (on_drag_end)
147 on_drag_end();
148 }
149}
150
151void Slider::mouseup_event(MouseEvent& event)
152{
153 if (event.button() == MouseButton::Primary) {
154 end_drag();
155 return;
156 }
157
158 return Widget::mouseup_event(event);
159}
160
161void Slider::mousewheel_event(MouseEvent& event)
162{
163 auto acceleration_modifier = step();
164 auto wheel_delta = event.wheel_delta_y();
165
166 if (event.modifiers() == KeyModifier::Mod_Ctrl)
167 acceleration_modifier *= 6;
168 if (knob_size_mode() == KnobSizeMode::Proportional)
169 wheel_delta /= abs(wheel_delta);
170
171 if (orientation() == Orientation::Horizontal)
172 decrease_slider_by(wheel_delta * acceleration_modifier);
173 else
174 increase_slider_by(wheel_delta * acceleration_modifier);
175
176 Widget::mousewheel_event(event);
177}
178
179void Slider::leave_event(Core::Event& event)
180{
181 if (!is_enabled())
182 return;
183 set_knob_hovered(false);
184 Widget::leave_event(event);
185}
186
187void Slider::change_event(Event& event)
188{
189 if (event.type() == Event::Type::EnabledChange) {
190 if (!is_enabled())
191 m_dragging = false;
192 }
193 Widget::change_event(event);
194}
195
196void Slider::set_knob_hovered(bool hovered)
197{
198 if (m_knob_hovered == hovered)
199 return;
200 m_knob_hovered = hovered;
201 update(knob_rect());
202}
203
204Optional<UISize> Slider::calculated_min_size() const
205{
206 if (orientation() == Gfx::Orientation::Vertical)
207 return { { knob_secondary_size(), knob_fixed_primary_size() * 2 + track_margin() * 2 } };
208 return { { knob_fixed_primary_size() * 2 + track_margin() * 2, knob_secondary_size() } };
209}
210
211Optional<UISize> Slider::calculated_preferred_size() const
212{
213 if (orientation() == Gfx::Orientation::Vertical)
214 return { { SpecialDimension::Shrink, SpecialDimension::OpportunisticGrow } };
215 return { { SpecialDimension::OpportunisticGrow, SpecialDimension::Shrink } };
216}
217
218}