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