HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
scroll_bar_widget.hpp
Go to the documentation of this file.
1// Copyright Take Vos 2020-2022.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
9#pragma once
10
11#include "widget.hpp"
12#include "../GUI/gui_event.hpp"
13#include "../geometry/axis.hpp"
14#include "../observer.hpp"
15#include <memory>
16#include <string>
17#include <array>
18#include <optional>
19#include <future>
20
21namespace hi { inline namespace v1 {
22
32template<axis Axis>
33class scroll_bar_widget final : public widget {
34public:
35 using super = widget;
36
37 static constexpr hi::axis axis = Axis;
38
39 observer<float> offset;
40 observer<float> aperture;
41 observer<float> content;
42
45 forward_of<observer<float>> auto&& content,
46 forward_of<observer<float>> auto&& aperture,
47 forward_of<observer<float>> auto&& offset) noexcept :
48 widget(parent), content(hi_forward(content)), aperture(hi_forward(aperture)), offset(hi_forward(offset))
49 {
50 _content_cbt = this->content.subscribe([&](auto...) {
51 ++global_counter<"scroll_bar_widget:content:relayout">;
52 process_event({gui_event_type::window_relayout});
53 });
54 _aperture_cbt = this->aperture.subscribe([&](auto...) {
55 ++global_counter<"scroll_bar_widget:aperture:relayout">;
56 process_event({gui_event_type::window_relayout});
57 });
58 _offset_cbt = this->offset.subscribe([&](auto...) {
59 ++global_counter<"scroll_bar_widget:offset:relayout">;
60 process_event({gui_event_type::window_relayout});
61 });
62 }
63
65
66 widget_constraints const& set_constraints(set_constraints_context const& context) noexcept override
67 {
68 _layout = {};
69
70 // The minimum size is twice the length of the slider, which is twice the context.theme->size()
71 if constexpr (axis == axis::vertical) {
72 return _constraints = {
73 {context.theme->icon_size, context.theme->size * 4.0f},
74 {context.theme->icon_size, context.theme->size * 4.0f},
75 {context.theme->icon_size, 32767.0f}};
76 } else {
77 return _constraints = {
78 {context.theme->size * 4.0f, context.theme->icon_size},
79 {context.theme->size * 4.0f, context.theme->icon_size},
80 {32767.0f, context.theme->icon_size}};
81 }
82 }
83
84 void set_layout(widget_layout const& context) noexcept override
85 {
86 _layout = context;
87
88 // Calculate the position of the slider.
89 hilet slider_offset = *offset * travel_vs_hidden_content_ratio();
90 if constexpr (axis == axis::vertical) {
91 _slider_rectangle = aarectangle{0.0f, slider_offset, context.width(), slider_length()};
92 } else {
93 _slider_rectangle = aarectangle{slider_offset, 0.0f, slider_length(), context.height()};
94 }
95 }
96
97 void draw(draw_context const& context) noexcept override
98 {
99 if (*mode > widget_mode::invisible and overlaps(context, layout())) {
100 draw_rails(context);
101 draw_slider(context);
102 }
103 }
104
105 hitbox hitbox_test(point3 position) const noexcept override
106 {
107 hi_axiom(loop::main().on_thread());
108
109 if (*mode >= widget_mode::partial and layout().contains(position) and _slider_rectangle.contains(position)) {
110 return {this, position, hitbox_type::scroll_bar};
111 } else {
112 return {};
113 }
114 }
115
116 bool handle_event(gui_event const& event) noexcept
117 {
118 switch (event.type()) {
119 case gui_event_type::mouse_down:
120 if (event.mouse().cause.left_button) {
121 // Record the original scroll-position before the drag starts.
122 _offset_before_drag = *offset;
123 return true;
124 }
125 break;
126
127 case gui_event_type::mouse_drag:
128 if (event.mouse().cause.left_button) {
129 // The distance the slider has to move relative to the slider position at the
130 // start of the drag.
131 hilet slider_movement = axis == axis::vertical ? event.drag_delta().y() : event.drag_delta().x();
132 hilet content_movement = slider_movement * hidden_content_vs_travel_ratio();
133 hilet new_offset = _offset_before_drag + content_movement;
134 offset = clamp_offset(new_offset);
135 return true;
136 }
137 break;
138
139 default:;
140 }
141
142 return super::handle_event(event);
143 }
144
145 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
146 {
147 return false;
148 }
149
150 [[nodiscard]] color background_color() const noexcept override
151 {
152 return _layout.theme->color(semantic_color::fill, semantic_layer);
153 }
154
155 [[nodiscard]] color foreground_color() const noexcept override
156 {
157 if (*hover) {
158 return _layout.theme->color(semantic_color::fill, semantic_layer + 2);
159 } else {
160 return _layout.theme->color(semantic_color::fill, semantic_layer + 1);
161 }
162 }
163
164private:
165 aarectangle _slider_rectangle;
166
167 float _offset_before_drag;
168
169 typename decltype(content)::callback_token _content_cbt;
170 typename decltype(aperture)::callback_token _aperture_cbt;
171 typename decltype(offset)::callback_token _offset_cbt;
172
177 [[nodiscard]] float clamp_offset(float new_offset) const noexcept
178 {
179 hilet scrollable_distance = std::max(0.0f, *content - *aperture);
180 return std::clamp(new_offset, 0.0f, scrollable_distance);
181 }
182
183 [[nodiscard]] float rail_length() const noexcept
184 {
185 hi_axiom(loop::main().on_thread());
186 return axis == axis::vertical ? layout().height() : layout().width();
187 }
188
189 [[nodiscard]] float slider_length() const noexcept
190 {
191 hi_axiom(loop::main().on_thread());
192
193 hilet content_aperture_ratio = *content != 0.0f ? *aperture / *content : 1.0f;
194 hilet rail_length_ = rail_length();
195 return std::clamp(rail_length_ * content_aperture_ratio, _layout.theme->size * 2.0f, rail_length_);
196 }
197
200 [[nodiscard]] float slider_travel_range() const noexcept
201 {
202 hi_axiom(loop::main().on_thread());
203 return rail_length() - slider_length();
204 }
205
208 [[nodiscard]] float hidden_content() const noexcept
209 {
210 hi_axiom(loop::main().on_thread());
211 return *content - *aperture;
212 }
213
218 [[nodiscard]] float hidden_content_vs_travel_ratio() const noexcept
219 {
220 hi_axiom(loop::main().on_thread());
221
222 hilet _slider_travel_range = slider_travel_range();
223 return _slider_travel_range != 0.0f ? hidden_content() / _slider_travel_range : 0.0f;
224 }
225
230 [[nodiscard]] float travel_vs_hidden_content_ratio() const noexcept
231 {
232 hi_axiom(loop::main().on_thread());
233
234 hilet _hidden_content = hidden_content();
235 return _hidden_content != 0.0f ? slider_travel_range() / _hidden_content : 0.0f;
236 }
237
238 void draw_rails(draw_context const& context) noexcept
239 {
240 hilet corner_radii =
241 axis == axis::vertical ? hi::corner_radii{layout().width() * 0.5f} : hi::corner_radii{layout().height() * 0.5f};
242 context.draw_box(layout(), layout().rectangle(), background_color(), corner_radii);
243 }
244
245 void draw_slider(draw_context const& context) noexcept
246 {
247 hilet corner_radii = axis == axis::vertical ? hi::corner_radii{_slider_rectangle.width() * 0.5f} :
248 hi::corner_radii{_slider_rectangle.height() * 0.5f};
249
250 context.draw_box(layout(), translate_z(0.1f) * _slider_rectangle, foreground_color(), corner_radii);
251 }
252};
253
254using horizontal_scroll_bar_widget = scroll_bar_widget<axis::horizontal>;
255using vertical_scroll_bar_widget = scroll_bar_widget<axis::vertical>;
256
257}} // namespace hi::v1
#define hi_axiom(expression)
Specify an axiom; an expression that is true.
Definition assert.hpp:133
#define hilet
Invariant should be the default for variables.
Definition utility.hpp:23
#define hi_forward(x)
Forward a value, based on the decltype of the value.
Definition utility.hpp:29
Defines widget.
Definition of GUI event types.
@ window_relayout
Request that widgets get laid out on the next frame.
@ rectangle
The gui_event has rectangle data.
@ partial
A widget is partially enabled.
@ invisible
The widget is invisible.
DOXYGEN BUG.
Definition algorithm.hpp:15
The HikoGUI namespace.
Definition ascii.hpp:19
A user interface event.
Definition gui_event.hpp:76
Scroll bar widget This widget is used in a pair of a vertical and horizontal scrollbar as a child of ...
Definition scroll_bar_widget.hpp:33
hitbox hitbox_test(point3 position) const noexcept override
Find the widget that is under the mouse cursor.
Definition scroll_bar_widget.hpp:105
void draw(draw_context const &context) noexcept override
Draw the widget.
Definition scroll_bar_widget.hpp:97
void set_layout(widget_layout const &context) noexcept override
Update the internal layout of the widget.
Definition scroll_bar_widget.hpp:84
widget_constraints const & set_constraints(set_constraints_context const &context) noexcept override
Update the constraints of the widget.
Definition scroll_bar_widget.hpp:66
bool handle_event(gui_event const &event) noexcept
Handle command.
Definition scroll_bar_widget.hpp:116
bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
Check if the widget will accept keyboard focus.
Definition scroll_bar_widget.hpp:145
Definition set_constraints_context.hpp:15
An interactive graphical object as part of the user-interface.
Definition widget.hpp:45
widget_layout const & layout() const noexcept
Get the current layout for this widget.
Definition widget.hpp:181
int semantic_layer
The draw layer of the widget.
Definition widget.hpp:83
observer< bool > hover
Mouse cursor is hovering over the widget.
Definition widget.hpp:63
widget(widget *parent) noexcept
virtual bool handle_event(gui_event const &event) noexcept
Handle command.
widget * parent
Pointer to the parent widget.
Definition widget.hpp:50
observer< widget_mode > mode
The widget mode.
Definition widget.hpp:59
The constraints of a widget.
Definition widget_constraints.hpp:26
The layout of a widget.
Definition widget_layout.hpp:40
T max(T... args)