HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
theme.hpp
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
5#pragma once
6
7#include "../settings/settings.hpp"
8#include "../text/module.hpp"
9#include "../utility/utility.hpp"
10#include "../color/module.hpp"
11#include "../geometry/module.hpp"
12#include "../codec/codec.hpp"
13#include "../macros.hpp"
14#include <array>
15#include <filesystem>
16#include <string>
17#include <vector>
18
19namespace hi::inline v1 {
20
21class theme {
22public:
25 float dpi = 72;
26
29 float scale = 1.0f;
30
31 std::string name;
32 theme_mode mode = theme_mode::light;
33
34 theme() noexcept = default;
35 theme(theme const&) noexcept = default;
36 theme(theme&&) noexcept = default;
37 theme& operator=(theme const&) noexcept = default;
38 theme& operator=(theme&&) noexcept = default;
39
42 theme(std::filesystem::path const& path)
43 {
44 try {
45 hi_log_info("Parsing theme at {}", path.string());
46 hilet data = parse_JSON(path);
47 parse(data);
48 } catch (std::exception const& e) {
49 throw io_error(std::format("{}: Could not load theme.\n{}", path.string(), e.what()));
50 }
51 }
52
55 template<typename T = hi::margins>
56 [[nodiscard]] constexpr T margin() const noexcept
57 {
58 if constexpr (std::is_same_v<T, hi::margins>) {
59 return hi::margins{_margin};
60 } else if constexpr (std::is_same_v<T, float>) {
61 return _margin;
62 } else {
63 hi_static_not_implemented();
64 }
65 }
66
69 [[nodiscard]] constexpr float border_width() const noexcept
70 {
71 return _border_width;
72 }
73
76 template<typename T = hi::corner_radii>
77 [[nodiscard]] constexpr T rounding_radius() const noexcept
78 {
79 if constexpr (std::is_same_v<T, hi::corner_radii>) {
80 return T{_rounding_radius};
81 } else if constexpr (std::is_same_v<T, float>) {
82 return _rounding_radius;
83 } else {
84 hi_static_not_implemented();
85 }
86 }
87
90 [[nodiscard]] constexpr float size() const noexcept
91 {
92 return _size;
93 }
94
97 [[nodiscard]] constexpr float large_size() const noexcept
98 {
99 return _large_size;
100 }
101
104 [[nodiscard]] constexpr float icon_size() const noexcept
105 {
106 return _icon_size;
107 }
108
111 [[nodiscard]] constexpr float large_icon_size() const noexcept
112 {
113 return _large_icon_size;
114 }
115
118 [[nodiscard]] constexpr float label_icon_size() const noexcept
119 {
120 return _label_icon_size;
121 }
122
125 [[nodiscard]] constexpr float baseline_adjustment() const noexcept
126 {
127 return _baseline_adjustment;
128 }
129
140 [[nodiscard]] theme transform(float new_dpi) const noexcept
141 {
142 auto r = *this;
143
144 hi_assert(new_dpi != 0.0f);
145 hi_assert(dpi != 0.0f);
146 hi_assert(scale != 0.0f);
147
148 auto delta_scale = new_dpi / dpi;
149 r.dpi = new_dpi;
150 r.scale = delta_scale * scale;
151
152 // Scale each size, and round so that everything will stay aligned on pixel boundaries.
153 r._margin = std::round(delta_scale * _margin);
154 r._border_width = std::round(delta_scale * _border_width);
155 r._rounding_radius = std::round(delta_scale * _rounding_radius);
156 r._size = std::round(delta_scale * _size);
157 r._large_size = std::round(delta_scale * _large_size);
158 r._icon_size = std::round(delta_scale * _icon_size);
159 r._large_icon_size = std::round(delta_scale * _large_icon_size);
160 r._label_icon_size = std::round(delta_scale * _label_icon_size);
161 // Cap height is not rounded, since the text-shaper will align the text to sub-pixel boundaries.
162 r._baseline_adjustment = std::round(delta_scale * _baseline_adjustment);
163
164 return r;
165 }
166
167 [[nodiscard]] hi::color color(hi::semantic_color original_color, ssize_t nesting_level = 0) const noexcept
168 {
169 hilet& shades = _colors[std::to_underlying(original_color)];
170 hi_assert(not shades.empty());
171
172 nesting_level = std::max(ssize_t{0}, nesting_level);
173 return shades[nesting_level % ssize(shades)];
174 }
175
176 [[nodiscard]] hi::color color(hi::color original_color, ssize_t nesting_level = 0) const noexcept
177 {
178 if (original_color.is_semantic()) {
179 return color(static_cast<semantic_color>(original_color), nesting_level);
180 } else {
181 return original_color;
182 }
183 }
184
185 [[nodiscard]] hi::text_style text_style(semantic_text_style theme_color) const noexcept
186 {
187 return _text_styles[std::to_underlying(theme_color)];
188 }
189
190 [[nodiscard]] hi::text_style text_style(hi::text_style original_style) const noexcept
191 {
192 if (original_style.is_semantic()) {
193 return text_style(static_cast<semantic_text_style>(original_style));
194 } else {
195 return original_style;
196 }
197 }
198
199private:
202 float _margin = 5.0f;
203
206 float _border_width = 1.0f;
207
210 float _rounding_radius = 4.0f;
211
214 float _size = 11.0f;
215
218 float _large_size = 19.0f;
219
222 float _icon_size = 8.0f;
223
226 float _large_icon_size = 23.0f;
227
230 float _label_icon_size = 15.0f;
231
234 float _baseline_adjustment = 9.0f;
235
236 std::array<std::vector<hi::color>, semantic_color_metadata.size()> _colors;
237 std::array<hi::text_style, semantic_text_style_metadata.size()> _text_styles;
238
239 [[nodiscard]] float parse_float(datum const& data, char const *object_name)
240 {
241 if (!data.contains(object_name)) {
242 throw parse_error(std::format("Missing '{}'", object_name));
243 }
244
245 hilet object = data[object_name];
246 if (auto f = get_if<double>(object)) {
247 return static_cast<float>(*f);
248 } else if (auto ll = get_if<long long>(object)) {
249 return static_cast<float>(*ll);
250 } else {
251 throw parse_error(
252 std::format("'{}' attribute must be a floating point number, got {}.", object_name, object.type_name()));
253 }
254 }
255
256 [[nodiscard]] long long parse_long_long(datum const& data, char const *object_name)
257 {
258 if (!data.contains(object_name)) {
259 throw parse_error(std::format("Missing '{}'", object_name));
260 }
261
262 hilet object = data[object_name];
263 if (auto f = get_if<long long>(object)) {
264 return static_cast<long long>(*f);
265 } else {
266 throw parse_error(std::format("'{}' attribute must be a integer, got {}.", object_name, object.type_name()));
267 }
268 }
269
270 [[nodiscard]] int parse_int(datum const& data, char const *object_name)
271 {
272 hilet value = parse_long_long(data, object_name);
274 throw parse_error(std::format("'{}' attribute is out of range, got {}.", object_name, value));
275 }
276 return narrow_cast<int>(value);
277 }
278
279 [[nodiscard]] bool parse_bool(datum const& data, char const *object_name)
280 {
281 if (!data.contains(object_name)) {
282 throw parse_error(std::format("Missing '{}'", object_name));
283 }
284
285 hilet object = data[object_name];
286 if (!holds_alternative<bool>(object)) {
287 throw parse_error(std::format("'{}' attribute must be a boolean, got {}.", object_name, object.type_name()));
288 }
289
290 return to_bool(object);
291 }
292
293 [[nodiscard]] std::string parse_string(datum const& data, char const *object_name)
294 {
295 // Extract name
296 if (!data.contains(object_name)) {
297 throw parse_error(std::format("Missing '{}'", object_name));
298 }
299 hilet object = data[object_name];
300 if (!holds_alternative<std::string>(object)) {
301 throw parse_error(std::format("'{}' attribute must be a string, got {}.", object_name, object.type_name()));
302 }
303 return static_cast<std::string>(object);
304 }
305
306 [[nodiscard]] hi::color parse_color_value(datum const& data)
307 {
308 if (holds_alternative<datum::vector_type>(data)) {
309 if (data.size() != 3 && data.size() != 4) {
310 throw parse_error(std::format("Expect 3 or 4 values for a color, got {}.", data));
311 }
312 hilet r = data[0];
313 hilet g = data[1];
314 hilet b = data[2];
315 hilet a = data.size() == 4 ? data[3] : (holds_alternative<long long>(r) ? datum{255} : datum{1.0});
316
317 if (holds_alternative<long long>(r) and holds_alternative<long long>(g) and holds_alternative<long long>(b) and
318 holds_alternative<long long>(a)) {
319 hilet r_ = get<long long>(r);
320 hilet g_ = get<long long>(g);
321 hilet b_ = get<long long>(b);
322 hilet a_ = get<long long>(a);
323
324 hi_check(r_ >= 0 and r_ <= 255, "integer red-color value not within 0 and 255");
325 hi_check(g_ >= 0 and g_ <= 255, "integer green-color value not within 0 and 255");
326 hi_check(b_ >= 0 and b_ <= 255, "integer blue-color value not within 0 and 255");
327 hi_check(a_ >= 0 and a_ <= 255, "integer alpha-color value not within 0 and 255");
328
329 return color_from_sRGB(
330 static_cast<uint8_t>(r_), static_cast<uint8_t>(g_), static_cast<uint8_t>(b_), static_cast<uint8_t>(a_));
331
332 } else if (
333 holds_alternative<double>(r) and holds_alternative<double>(g) and holds_alternative<double>(b) and
334 holds_alternative<double>(a)) {
335 hilet r_ = static_cast<float>(get<double>(r));
336 hilet g_ = static_cast<float>(get<double>(g));
337 hilet b_ = static_cast<float>(get<double>(b));
338 hilet a_ = static_cast<float>(get<double>(a));
339
340 return hi::color(r_, g_, b_, a_);
341
342 } else {
343 throw parse_error(std::format("Expect all integers or all floating point numbers in a color, got {}.", data));
344 }
345
346 } else if (hilet *color_name = get_if<std::string>(data)) {
347 hilet color_name_ = to_lower(*color_name);
348 if (color_name_.starts_with("#")) {
349 return color_from_sRGB(color_name_);
350
351 } else {
352 throw parse_error(std::format("Unable to parse color, got {}.", data));
353 }
354 } else {
355 throw parse_error(std::format("Unable to parse color, got {}.", data));
356 }
357 }
358
359 [[nodiscard]] hi::color parse_color(datum const& data, char const *object_name)
360 {
361 if (!data.contains(object_name)) {
362 throw parse_error(std::format("Missing color '{}'", object_name));
363 }
364
365 hilet color_object = data[object_name];
366
367 try {
368 return parse_color_value(color_object);
369 } catch (parse_error const&) {
370 if (auto s = get_if<std::string>(color_object)) {
372 } else {
373 throw;
374 }
375 }
376 }
377
378 [[nodiscard]] std::vector<hi::color> parse_color_list(datum const& data, char const *object_name)
379 {
380 // Extract name
381 if (!data.contains(object_name)) {
382 throw parse_error(std::format("Missing color list '{}'", object_name));
383 }
384
385 hilet color_list_object = data[object_name];
386 if (holds_alternative<datum::vector_type>(color_list_object) and not color_list_object.empty() and
387 holds_alternative<datum::vector_type>(color_list_object[0])) {
388 auto r = std::vector<hi::color>{};
389 ssize_t i = 0;
390 for (hilet& color : color_list_object) {
391 try {
392 r.push_back(parse_color_value(color));
393 } catch (parse_error const& e) {
394 throw parse_error(
395 std::format("Could not parse {}nd entry of color list '{}'\n{}", i + 1, object_name, e.what()));
396 }
397 }
398 return r;
399
400 } else {
401 try {
402 return {parse_color_value(data[object_name])};
403 } catch (parse_error const& e) {
404 throw parse_error(std::format("Could not parse color '{}'\n{}", object_name, e.what()));
405 }
406 }
407 }
408
409 [[nodiscard]] hi::text_style parse_text_style_value(datum const& data)
410 {
411 if (!holds_alternative<datum::map_type>(data)) {
412 throw parse_error(std::format("Expect a text-style to be an object, got '{}'", data));
413 }
414
415 hilet family_id = find_font_family(parse_string(data, "family"));
416 hilet font_size = parse_float(data, "size");
417
418 auto variant = font_variant{};
419 if (data.contains("weight")) {
420 variant.set_weight(parse_font_weight(data, "weight"));
421 } else {
422 variant.set_weight(font_weight::regular);
423 }
424
425 if (data.contains("italic")) {
426 variant.set_style(parse_bool(data, "italic") ? font_style::italic : font_style::normal);
427 } else {
428 variant.set_style(font_style::normal);
429 }
430
431 // resolve semantic color.
432 hilet color = this->color(parse_color(data, "color"), 0);
433
434 auto sub_styles = std::vector<text_sub_style>{};
435 sub_styles.emplace_back(
436 phrasing_mask::all, iso_639{}, iso_15924{}, family_id, variant, font_size, color, text_decoration{});
437 return hi::text_style(sub_styles);
438 }
439
440 [[nodiscard]] font_weight parse_font_weight(datum const& data, char const *object_name)
441 {
442 if (!data.contains(object_name)) {
443 throw parse_error(std::format("Missing '{}'", object_name));
444 }
445
446 hilet object = data[object_name];
447 if (auto i = get_if<long long>(object)) {
448 return font_weight_from_int(*i);
449 } else if (auto s = get_if<std::string>(object)) {
450 return font_weight_from_string(*s);
451 } else {
452 throw parse_error(std::format("Unable to parse font weight, got {}.", object.type_name()));
453 }
454 }
455
456 [[nodiscard]] hi::text_style parse_text_style(datum const& data, char const *object_name)
457 {
458 // Extract name
459 if (!data.contains(object_name)) {
460 throw parse_error(std::format("Missing text-style '{}'", object_name));
461 }
462
463 hilet textStyleObject = data[object_name];
464 try {
465 return parse_text_style_value(textStyleObject);
466 } catch (parse_error const& e) {
467 throw parse_error(std::format("Could not parse text-style '{}'\n{}", object_name, e.what()));
468 }
469 }
470
471 void parse(datum const& data)
472 {
473 hi_assert(holds_alternative<datum::map_type>(data));
474
475 name = parse_string(data, "name");
476
477 hilet mode_name = to_lower(parse_string(data, "mode"));
478 if (mode_name == "light") {
479 mode = theme_mode::light;
480 } else if (mode_name == "dark") {
481 mode = theme_mode::dark;
482 } else {
483 throw parse_error(std::format("Attribute 'mode' must be \"light\" or \"dark\", got \"{}\".", mode_name));
484 }
485
486 std::get<std::to_underlying(semantic_color::blue)>(_colors) = parse_color_list(data, "blue");
487 std::get<std::to_underlying(semantic_color::green)>(_colors) = parse_color_list(data, "green");
488 std::get<std::to_underlying(semantic_color::indigo)>(_colors) = parse_color_list(data, "indigo");
489 std::get<std::to_underlying(semantic_color::orange)>(_colors) = parse_color_list(data, "orange");
490 std::get<std::to_underlying(semantic_color::pink)>(_colors) = parse_color_list(data, "pink");
491 std::get<std::to_underlying(semantic_color::purple)>(_colors) = parse_color_list(data, "purple");
492 std::get<std::to_underlying(semantic_color::red)>(_colors) = parse_color_list(data, "red");
493 std::get<std::to_underlying(semantic_color::teal)>(_colors) = parse_color_list(data, "teal");
494 std::get<std::to_underlying(semantic_color::yellow)>(_colors) = parse_color_list(data, "yellow");
495
496 std::get<std::to_underlying(semantic_color::gray)>(_colors) = parse_color_list(data, "gray");
497 std::get<std::to_underlying(semantic_color::gray2)>(_colors) = parse_color_list(data, "gray2");
498 std::get<std::to_underlying(semantic_color::gray3)>(_colors) = parse_color_list(data, "gray3");
499 std::get<std::to_underlying(semantic_color::gray4)>(_colors) = parse_color_list(data, "gray4");
500 std::get<std::to_underlying(semantic_color::gray5)>(_colors) = parse_color_list(data, "gray5");
501 std::get<std::to_underlying(semantic_color::gray6)>(_colors) = parse_color_list(data, "gray6");
502
503 std::get<std::to_underlying(semantic_color::foreground)>(_colors) = parse_color_list(data, "foreground-color");
504 std::get<std::to_underlying(semantic_color::border)>(_colors) = parse_color_list(data, "border-color");
505 std::get<std::to_underlying(semantic_color::fill)>(_colors) = parse_color_list(data, "fill-color");
506 std::get<std::to_underlying(semantic_color::accent)>(_colors) = parse_color_list(data, "accent-color");
507 std::get<std::to_underlying(semantic_color::text_select)>(_colors) = parse_color_list(data, "text-select-color");
508 std::get<std::to_underlying(semantic_color::primary_cursor)>(_colors) = parse_color_list(data, "primary-cursor-color");
509 std::get<std::to_underlying(semantic_color::secondary_cursor)>(_colors) =
510 parse_color_list(data, "secondary-cursor-color");
511
512 std::get<std::to_underlying(semantic_text_style::label)>(_text_styles) = parse_text_style(data, "label-style");
513 std::get<std::to_underlying(semantic_text_style::small_label)>(_text_styles) =
514 parse_text_style(data, "small-label-style");
515 std::get<std::to_underlying(semantic_text_style::warning)>(_text_styles) = parse_text_style(data, "warning-label-style");
516 std::get<std::to_underlying(semantic_text_style::error)>(_text_styles) = parse_text_style(data, "error-label-style");
517 std::get<std::to_underlying(semantic_text_style::help)>(_text_styles) = parse_text_style(data, "help-label-style");
518 std::get<std::to_underlying(semantic_text_style::placeholder)>(_text_styles) =
519 parse_text_style(data, "placeholder-label-style");
520 std::get<std::to_underlying(semantic_text_style::link)>(_text_styles) = parse_text_style(data, "link-label-style");
521
522 _margin = narrow_cast<float>(parse_int(data, "margin"));
523 _border_width = narrow_cast<float>(parse_int(data, "border-width"));
524 _rounding_radius = narrow_cast<float>(parse_int(data, "rounding-radius"));
525 _size = narrow_cast<float>(parse_int(data, "size"));
526 _large_size = narrow_cast<float>(parse_int(data, "large-size"));
527 _icon_size = narrow_cast<float>(parse_int(data, "icon-size"));
528 _large_icon_size = narrow_cast<float>(parse_int(data, "large-icon-size"));
529 _label_icon_size = narrow_cast<float>(parse_int(data, "label-icon-size"));
530
531 _baseline_adjustment = std::ceil(std::get<std::to_underlying(semantic_text_style::label)>(_text_styles)->cap_height());
532 }
533
534 [[nodiscard]] friend std::string to_string(theme const& rhs) noexcept
535 {
536 return std::format("{}:{}", rhs.name, rhs.mode);
537 }
538
539 friend std::ostream& operator<<(std::ostream& lhs, theme const& rhs)
540 {
541 return lhs << to_string(rhs);
542 }
543};
544
545} // namespace hi::inline v1
semantic_color semantic_color_from_string(std::string_view str)
Convert a string to a semantic color.
Definition semantic_color.hpp:92
semantic_color
Semantic colors.
Definition semantic_color.hpp:23
color color_from_sRGB(float r, float g, float b, float a) noexcept
Convert gama corrected sRGB color to the linear color.
Definition sRGB.hpp:142
STL namespace.
DOXYGEN BUG.
Definition algorithm.hpp:16
hi_export constexpr font_weight font_weight_from_int(numeric_integral auto rhs)
Convert a font weight value between 50 and 1000 to a font weight.
Definition font_weight.hpp:60
font_weight
Definition font_weight.hpp:18
text_decoration
Describes how a grapheme should be underlined when rendering the text.
Definition text_decoration.hpp:23
hi_export font_family_id find_font_family(std::string const &family_name) noexcept
Find font family id.
Definition font_book.hpp:400
std::ptrdiff_t ssize_t
Signed size/index into an array.
Definition misc.hpp:33
font_style
The different styles a font-family comes with.
Definition font_style.hpp:26
constexpr Out narrow_cast(In const &rhs) noexcept
Cast numeric values without loss of precision.
Definition cast.hpp:377
This is a RGBA floating point color.
Definition color.hpp:45
The left, bottom, right and top margins.
Definition margins.hpp:23
Definition theme.hpp:21
constexpr float large_size() const noexcept
The size of large widgets.
Definition theme.hpp:97
constexpr float baseline_adjustment() const noexcept
The amount the base-line needs to be moved downwards when a label is aligned to top.
Definition theme.hpp:125
constexpr float label_icon_size() const noexcept
Size of icons being inline with a label's text.
Definition theme.hpp:118
constexpr T rounding_radius() const noexcept
The rounding radius of boxes with rounded corners.
Definition theme.hpp:77
constexpr float large_icon_size() const noexcept
Size of icons representing the length of am average word of a label's text.
Definition theme.hpp:111
constexpr float border_width() const noexcept
The line-width of a border.
Definition theme.hpp:69
constexpr float size() const noexcept
The size of small square widgets.
Definition theme.hpp:90
constexpr float icon_size() const noexcept
Size of icons inside a widget.
Definition theme.hpp:104
constexpr T margin() const noexcept
Distance between widgets and between widgets and the border of the container.
Definition theme.hpp:56
theme transform(float new_dpi) const noexcept
Create a transformed copy of the theme.
Definition theme.hpp:140
T ceil(T... args)
T emplace_back(T... args)
T max(T... args)
T round(T... args)
T to_string(T... args)
T what(T... args)