Serenity Operating System
1/*
2 * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2022, the SerenityOS developers.
4 * Copyright (c) 2023, networkException <networkexception@serenityos.org>
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include <LibGUI/OpacitySlider.h>
10#include <LibGUI/Painter.h>
11#include <LibGfx/Palette.h>
12#include <LibGfx/StylePainter.h>
13
14REGISTER_WIDGET(GUI, HorizontalOpacitySlider)
15REGISTER_WIDGET(GUI, VerticalOpacitySlider)
16
17namespace GUI {
18
19OpacitySlider::OpacitySlider(Gfx::Orientation orientation)
20 : AbstractSlider(orientation)
21{
22 set_min(0);
23 set_max(100);
24 set_value(100);
25 set_preferred_size(SpecialDimension::Fit);
26}
27
28Gfx::IntRect OpacitySlider::frame_inner_rect() const
29{
30 return rect().shrunken(4, 4);
31}
32
33void OpacitySlider::paint_event(PaintEvent& event)
34{
35 GUI::Painter painter(*this);
36 painter.add_clip_rect(event.rect());
37
38 auto inner_rect = frame_inner_rect();
39
40 // Grid pattern
41 Gfx::StylePainter::paint_transparency_grid(painter, inner_rect, palette());
42
43 // Alpha gradient
44 for (int pos = inner_rect.first_edge_for_orientation(orientation()); pos <= inner_rect.last_edge_for_orientation(orientation()); ++pos) {
45 float relative_offset = (float)pos / (float)inner_rect.primary_size_for_orientation(orientation());
46 float alpha = relative_offset * 255.0f;
47 Color color = m_base_color.with_alpha(static_cast<u8>(AK::min(alpha, UINT8_MAX)));
48 if (orientation() == Gfx::Orientation::Horizontal) {
49 painter.fill_rect({ pos, inner_rect.top(), 1, inner_rect.height() }, color);
50 } else {
51 painter.fill_rect({ inner_rect.left(), pos, inner_rect.right(), 1 }, color);
52 }
53 }
54
55 constexpr int notch_size = 3;
56 if (orientation() == Gfx::Orientation::Horizontal) {
57 int notch_y_top = inner_rect.top() + notch_size;
58 int notch_y_bottom = inner_rect.bottom() - notch_size;
59 int notch_x = inner_rect.left() + ((float)value() / (float)max() * (float)inner_rect.width());
60
61 // Top notch
62 painter.set_pixel(notch_x, notch_y_top, palette().threed_shadow2());
63 for (int i = notch_size; i >= 0; --i) {
64 painter.set_pixel(notch_x - (i + 1), notch_y_top - i - 1, palette().threed_highlight());
65 for (int j = 0; j < i * 2; ++j) {
66 painter.set_pixel(notch_x - (i + 1) + j + 1, notch_y_top - i - 1, palette().button());
67 }
68 painter.set_pixel(notch_x + (i + 0), notch_y_top - i - 1, palette().threed_shadow1());
69 painter.set_pixel(notch_x + (i + 1), notch_y_top - i - 1, palette().threed_shadow2());
70 }
71
72 // Bottom notch
73 painter.set_pixel(notch_x, notch_y_bottom, palette().threed_shadow2());
74 for (int i = 0; i < notch_size; ++i) {
75 painter.set_pixel(notch_x - (i + 1), notch_y_bottom + i + 1, palette().threed_highlight());
76 for (int j = 0; j < i * 2; ++j) {
77 painter.set_pixel(notch_x - (i + 1) + j + 1, notch_y_bottom + i + 1, palette().button());
78 }
79 painter.set_pixel(notch_x + (i + 0), notch_y_bottom + i + 1, palette().threed_shadow1());
80 painter.set_pixel(notch_x + (i + 1), notch_y_bottom + i + 1, palette().threed_shadow2());
81 }
82
83 // Hairline
84 // NOTE: If we're in the whiter part of the gradient, the notch is painted as shadow between the notches.
85 // If we're in the darker part, the notch is painted as highlight.
86 // We adjust the hairline's x position so it lines up with the shadow/highlight of the notches.
87 u8 h = ((float)value() / (float)max()) * 255.0f;
88 if (h < 128)
89 painter.draw_line({ notch_x, notch_y_top }, { notch_x, notch_y_bottom }, Color(h, h, h, 255));
90 else
91 painter.draw_line({ notch_x - 1, notch_y_top }, { notch_x - 1, notch_y_bottom }, Color(h, h, h, 255));
92 } else {
93 int notch_x_left = inner_rect.left() + notch_size;
94 int notch_x_right = inner_rect.right() - notch_size;
95 int notch_y = inner_rect.top() + ((float)value() / (float)max() * (float)inner_rect.height());
96
97 // Left notch
98 painter.set_pixel(notch_x_left, notch_y, palette().threed_shadow2());
99 for (int i = notch_size; i >= 0; --i) {
100 painter.set_pixel(notch_x_left - i - 1, notch_y - (i + 1), palette().threed_highlight());
101 for (int j = 0; j < i * 2; ++j) {
102 painter.set_pixel(notch_x_left - i - 1, notch_y - (i + 1) + j + 1, palette().button());
103 }
104 painter.set_pixel(notch_x_left - i - 1, notch_y + (i + 0), palette().threed_shadow1());
105 painter.set_pixel(notch_x_left - i - 1, notch_y + (i + 1), palette().threed_shadow2());
106 }
107
108 // Bottom notch
109 painter.set_pixel(notch_x_right, notch_y, palette().threed_shadow2());
110 for (int i = 0; i < notch_size; ++i) {
111 painter.set_pixel(notch_x_right + i + 1, notch_y - (i + 1), palette().threed_highlight());
112 for (int j = 0; j < i * 2; ++j) {
113 painter.set_pixel(notch_x_right + i + 1, notch_y - (i + 1) + j + 1, palette().button());
114 }
115 painter.set_pixel(notch_x_right + i + 1, notch_y + (i + 0), palette().threed_shadow1());
116 painter.set_pixel(notch_x_right + i + 1, notch_y + (i + 1), palette().threed_shadow2());
117 }
118
119 // Hairline
120 // NOTE: See above
121 u8 h = ((float)value() / (float)max()) * 255.0f;
122 if (h < 128)
123 painter.draw_line({ notch_x_left, notch_y }, { notch_x_right, notch_y }, Color(h, h, h, 255));
124 else
125 painter.draw_line({ notch_x_left, notch_y - 1 }, { notch_x_right, notch_y - 1 }, Color(h, h, h, 255));
126 }
127
128 // Text label
129 // FIXME: better support text in vertical orientation, either by having a vertical option for draw_text, or only showing when there is enough space
130 auto percent_text = DeprecatedString::formatted("{}%", (int)((float)value() / (float)max() * 100.0f));
131 painter.draw_text(inner_rect.translated(1, 1), percent_text, Gfx::TextAlignment::Center, Color::Black);
132 painter.draw_text(inner_rect, percent_text, Gfx::TextAlignment::Center, Color::White);
133
134 // Frame
135 Gfx::StylePainter::paint_frame(painter, rect(), palette(), Gfx::FrameShape::Container, Gfx::FrameShadow::Sunken, 2);
136}
137
138int OpacitySlider::value_at(Gfx::IntPoint position) const
139{
140 auto inner_rect = frame_inner_rect();
141 auto relevant_position = position.primary_offset_for_orientation(orientation()),
142 begin_position = inner_rect.first_edge_for_orientation(orientation()),
143 end_position = inner_rect.last_edge_for_orientation(orientation());
144 if (relevant_position < begin_position)
145 return min();
146 if (relevant_position > end_position)
147 return max();
148 float relative_offset = (float)(relevant_position - begin_position) / (float)inner_rect.primary_size_for_orientation(orientation());
149
150 int range = max() - min();
151 return min() + (int)(relative_offset * (float)range);
152}
153
154void OpacitySlider::set_base_color(Gfx::Color base_color)
155{
156 m_base_color = base_color;
157 update();
158}
159
160void OpacitySlider::mousedown_event(MouseEvent& event)
161{
162 if (event.button() == MouseButton::Primary) {
163 m_dragging = true;
164 set_value(value_at(event.position()));
165 return;
166 }
167 AbstractSlider::mousedown_event(event);
168}
169
170void OpacitySlider::mousemove_event(MouseEvent& event)
171{
172 if (m_dragging) {
173 set_value(value_at(event.position()));
174 return;
175 }
176 AbstractSlider::mousemove_event(event);
177}
178
179void OpacitySlider::mouseup_event(MouseEvent& event)
180{
181 if (event.button() == MouseButton::Primary) {
182 m_dragging = false;
183 return;
184 }
185 AbstractSlider::mouseup_event(event);
186}
187
188void OpacitySlider::mousewheel_event(MouseEvent& event)
189{
190 decrease_slider_by(event.wheel_delta_y());
191}
192
193Optional<UISize> OpacitySlider::calculated_min_size() const
194{
195 if (orientation() == Gfx::Orientation::Vertical)
196 return { { 22, 40 } };
197 return { { 40, 22 } };
198}
199
200Optional<UISize> OpacitySlider::calculated_preferred_size() const
201{
202 if (orientation() == Gfx::Orientation::Vertical)
203 return { { SpecialDimension::Shrink, SpecialDimension::OpportunisticGrow } };
204 return { { SpecialDimension::OpportunisticGrow, SpecialDimension::Shrink } };
205}
206
207}