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 "../theme/theme.hpp"
14#include "../macros.hpp"
15#include <gsl/gsl>
16#include <array>
17#include <filesystem>
18#include <string>
19#include <vector>
20
21hi_export_module(hikogui.GUI : theme);
22
23hi_export namespace hi::inline v1 {
24class theme {
25public:
28 unit::pixel_density pixel_density = unit::pixel_density{unit::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(unit::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 accent_color(size_t nesting_level = 0) const noexcept
162 {
163 hi_assert(not _accent_colors.empty());
164 return _accent_colors[nesting_level % _accent_colors.size()];
165 }
166
167 [[nodiscard]] hi::color foreground_color(size_t nesting_level = 0) const noexcept
168 {
169 hi_assert(not _foreground_colors.empty());
170 return _foreground_colors[nesting_level % _foreground_colors.size()];
171 }
172
173 [[nodiscard]] hi::color border_color(size_t nesting_level = 0) const noexcept
174 {
175 hi_assert(not _border_colors.empty());
176 return _border_colors[nesting_level % _border_colors.size()];
177 }
178
179 [[nodiscard]] hi::color fill_color(size_t nesting_level = 0) const noexcept
180 {
181 hi_assert(not _fill_colors.empty());
182 return _fill_colors[nesting_level % _fill_colors.size()];
183 }
184
185 [[nodiscard]] hi::color text_select_color(size_t nesting_level = 0) const noexcept
186 {
187 hi_assert(not _text_select_colors.empty());
188 return _text_select_colors[nesting_level % _text_select_colors.size()];
189 }
190
191 [[nodiscard]] hi::color primary_cursor_color(size_t nesting_level = 0) const noexcept
192 {
193 hi_assert(not _primary_cursor_colors.empty());
194 return _primary_cursor_colors[nesting_level % _primary_cursor_colors.size()];
195 }
196
197 [[nodiscard]] hi::color secondary_cursor_color(size_t nesting_level = 0) const noexcept
198 {
199 hi_assert(not _secondary_cursor_colors.empty());
200 return _secondary_cursor_colors[nesting_level % _secondary_cursor_colors.size()];
201 }
202
203 [[nodiscard]] hi::text_style_set const &text_style_set() const noexcept
204 {
205 return _text_style_set;
206 }
207
208 [[nodiscard]] style::attributes_from_theme_type attributes_from_theme_function() const noexcept
209 {
210 return [](style_path const &path, style_pseudo_class const &pseudo_class) -> style_attributes {
211 return style_attributes{};
212 };
213 }
214
215private:
218 float _margin = 5.0f;
219
222 float _border_width = 1.0f;
223
226 float _rounding_radius = 4.0f;
227
230 float _size = 11.0f;
231
234 float _large_size = 19.0f;
235
238 float _icon_size = 8.0f;
239
242 float _large_icon_size = 23.0f;
243
246 float _label_icon_size = 15.0f;
247
250 float _baseline_adjustment = 9.0f;
251
252 std::vector<hi::color> _foreground_colors;
253 std::vector<hi::color> _border_colors;
254 std::vector<hi::color> _fill_colors;
255 std::vector<hi::color> _accent_colors;
256 std::vector<hi::color> _text_select_colors;
257 std::vector<hi::color> _primary_cursor_colors;
258 std::vector<hi::color> _secondary_cursor_colors;
259
260 hi::text_style_set _text_style_set;
261
262 [[nodiscard]] float parse_float(datum const& data, char const* object_name)
263 {
264 if (!data.contains(object_name)) {
265 throw parse_error(std::format("Missing '{}'", object_name));
266 }
267
268 auto const object = data[object_name];
269 if (auto f = get_if<double>(object)) {
270 return static_cast<float>(*f);
271 } else if (auto ll = get_if<long long>(object)) {
272 return static_cast<float>(*ll);
273 } else {
274 throw parse_error(
275 std::format("'{}' attribute must be a floating point number, got {}.", object_name, object.type_name()));
276 }
277 }
278
279 [[nodiscard]] long long parse_long_long(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 auto const object = data[object_name];
286 if (auto f = get_if<long long>(object)) {
287 return static_cast<long long>(*f);
288 } else {
289 throw parse_error(std::format("'{}' attribute must be a integer, got {}.", object_name, object.type_name()));
290 }
291 }
292
293 [[nodiscard]] int parse_int(datum const& data, char const* object_name)
294 {
295 auto const value = parse_long_long(data, object_name);
297 throw parse_error(std::format("'{}' attribute is out of range, got {}.", object_name, value));
298 }
299 return narrow_cast<int>(value);
300 }
301
302 [[nodiscard]] bool parse_bool(datum const& data, char const* object_name)
303 {
304 if (!data.contains(object_name)) {
305 throw parse_error(std::format("Missing '{}'", object_name));
306 }
307
308 auto const object = data[object_name];
309 if (!holds_alternative<bool>(object)) {
310 throw parse_error(std::format("'{}' attribute must be a boolean, got {}.", object_name, object.type_name()));
311 }
312
313 return to_bool(object);
314 }
315
316 [[nodiscard]] std::string parse_string(datum const& data, char const* object_name)
317 {
318 // Extract name
319 if (!data.contains(object_name)) {
320 throw parse_error(std::format("Missing '{}'", object_name));
321 }
322 auto const object = data[object_name];
323 if (!holds_alternative<std::string>(object)) {
324 throw parse_error(std::format("'{}' attribute must be a string, got {}.", object_name, object.type_name()));
325 }
326 return static_cast<std::string>(object);
327 }
328
329 [[nodiscard]] hi::color parse_color_value(datum const& data)
330 {
331 if (holds_alternative<datum::vector_type>(data)) {
332 if (data.size() != 3 && data.size() != 4) {
333 throw parse_error(std::format("Expect 3 or 4 values for a color, got {}.", data));
334 }
335 auto const r = data[0];
336 auto const g = data[1];
337 auto const b = data[2];
338 auto const a = data.size() == 4 ? data[3] : (holds_alternative<long long>(r) ? datum{255} : datum{1.0});
339
340 if (holds_alternative<long long>(r) and holds_alternative<long long>(g) and holds_alternative<long long>(b) and
341 holds_alternative<long long>(a)) {
342 auto const r_ = get<long long>(r);
343 auto const g_ = get<long long>(g);
344 auto const b_ = get<long long>(b);
345 auto const a_ = get<long long>(a);
346
347 hi_check(r_ >= 0 and r_ <= 255, "integer red-color value not within 0 and 255");
348 hi_check(g_ >= 0 and g_ <= 255, "integer green-color value not within 0 and 255");
349 hi_check(b_ >= 0 and b_ <= 255, "integer blue-color value not within 0 and 255");
350 hi_check(a_ >= 0 and a_ <= 255, "integer alpha-color value not within 0 and 255");
351
352 return color_from_sRGB(
353 static_cast<uint8_t>(r_), static_cast<uint8_t>(g_), static_cast<uint8_t>(b_), static_cast<uint8_t>(a_));
354
355 } else if (
356 holds_alternative<double>(r) and holds_alternative<double>(g) and holds_alternative<double>(b) and
357 holds_alternative<double>(a)) {
358 auto const r_ = static_cast<float>(get<double>(r));
359 auto const g_ = static_cast<float>(get<double>(g));
360 auto const b_ = static_cast<float>(get<double>(b));
361 auto const a_ = static_cast<float>(get<double>(a));
362
363 return hi::color(r_, g_, b_, a_);
364
365 } else {
366 throw parse_error(std::format("Expect all integers or all floating point numbers in a color, got {}.", data));
367 }
368
369 } else if (auto const* color_name = get_if<std::string>(data)) {
370 auto const color_name_ = to_lower(*color_name);
371 if (color_name_.starts_with("#")) {
372 return color_from_sRGB(color_name_);
373 } else if (auto color_ptr = color::find(color_name_)) {
374 return *color_ptr;
375 } else {
376 throw parse_error(std::format("Unable to parse color, got {}.", data));
377 }
378 } else {
379 throw parse_error(std::format("Unable to parse color, got {}.", data));
380 }
381 }
382
383 [[nodiscard]] hi::color parse_color(datum const& data, char const* object_name)
384 {
385 if (!data.contains(object_name)) {
386 throw parse_error(std::format("Missing color '{}'", object_name));
387 }
388
389 auto const color_object = data[object_name];
390
391 return parse_color_value(color_object);
392 }
393
394 [[nodiscard]] std::vector<hi::color> parse_color_list(datum const& data, char const* object_name)
395 {
396 // Extract name
397 if (!data.contains(object_name)) {
398 throw parse_error(std::format("Missing color list '{}'", object_name));
399 }
400
401 auto const color_list_object = data[object_name];
402 if (holds_alternative<datum::vector_type>(color_list_object) and not color_list_object.empty() and
403 holds_alternative<datum::vector_type>(color_list_object[0])) {
404 auto r = std::vector<hi::color>{};
405 ssize_t i = 0;
406 for (auto const& color : color_list_object) {
407 try {
408 r.push_back(parse_color_value(color));
409 } catch (parse_error const& e) {
410 throw parse_error(
411 std::format("Could not parse {}nd entry of color list '{}'\n{}", i + 1, object_name, e.what()));
412 }
413 }
414 return r;
415
416 } else {
417 try {
418 return {parse_color_value(data[object_name])};
419 } catch (parse_error const& e) {
420 throw parse_error(std::format("Could not parse color '{}'\n{}", object_name, e.what()));
421 }
422 }
423 }
424
425 [[nodiscard]] hi::text_style parse_text_style_value(datum const& data)
426 {
427 if (not holds_alternative<datum::map_type>(data)) {
428 throw parse_error(std::format("Expect a text-style to be an object, got '{}'", data));
429 }
430
431 auto r = hi::text_style{};
432
433 auto const family_id = find_font_family(parse_string(data, "family"));
434
435 auto variant = font_variant{};
436 if (data.contains("weight")) {
437 variant.set_weight(parse_font_weight(data, "weight"));
438 } else {
439 variant.set_weight(font_weight::regular);
440 }
441
442 if (data.contains("italic")) {
443 variant.set_style(parse_bool(data, "italic") ? font_style::italic : font_style::normal);
444 } else {
445 variant.set_style(font_style::normal);
446 }
447
448 auto font_id = find_font(family_id, variant);
449
450 r.set_font_chain({font_id});
451 r.set_size(unit::points_per_em(gsl::narrow<short>(parse_float(data, "size"))));
452 r.set_color(parse_color(data, "color"));
453 r.set_line_spacing(1.0f);
454 r.set_paragraph_spacing(1.5f);
455 return r;
456 }
457
458 [[nodiscard]] font_weight parse_font_weight(datum const& data, char const* object_name)
459 {
460 if (!data.contains(object_name)) {
461 throw parse_error(std::format("Missing '{}'", object_name));
462 }
463
464 auto const object = data[object_name];
465 if (auto i = get_if<long long>(object)) {
466 return font_weight_from_int(*i);
467 } else if (auto s = get_if<std::string>(object)) {
468 return font_weight_from_string(*s);
469 } else {
470 throw parse_error(std::format("Unable to parse font weight, got {}.", object.type_name()));
471 }
472 }
473
474 [[nodiscard]] hi::text_style parse_text_style(datum const& data, char const* object_name)
475 {
476 // Extract name
477 if (!data.contains(object_name)) {
478 throw parse_error(std::format("Missing text-style '{}'", object_name));
479 }
480
481 auto const textStyleObject = data[object_name];
482 try {
483 return parse_text_style_value(textStyleObject);
484 } catch (parse_error const& e) {
485 throw parse_error(std::format("Could not parse text-style '{}'\n{}", object_name, e.what()));
486 }
487 }
488
489 void parse(datum const& data)
490 {
491 hi_assert(holds_alternative<datum::map_type>(data));
492
493 name = parse_string(data, "name");
494
495 auto const mode_name = to_lower(parse_string(data, "mode"));
496 if (mode_name == "light") {
497 mode = theme_mode::light;
498 } else if (mode_name == "dark") {
499 mode = theme_mode::dark;
500 } else {
501 throw parse_error(std::format("Attribute 'mode' must be \"light\" or \"dark\", got \"{}\".", mode_name));
502 }
503
504 named_color<"blue"> = parse_color(data, "blue");
505 named_color<"green"> = parse_color(data, "green");
506 named_color<"indigo"> = parse_color(data, "indigo");
507 named_color<"orange"> = parse_color(data, "orange");
508 named_color<"pink"> = parse_color(data, "pink");
509 named_color<"purple"> = parse_color(data, "purple");
510 named_color<"red"> = parse_color(data, "red");
511 named_color<"teal"> = parse_color(data, "teal");
512 named_color<"yellow"> = parse_color(data, "yellow");
513
514 named_color<"gray0"> = parse_color(data, "gray0");
515 named_color<"gray1"> = parse_color(data, "gray1");
516 named_color<"gray2"> = parse_color(data, "gray2");
517 named_color<"gray3"> = parse_color(data, "gray3");
518 named_color<"gray4"> = parse_color(data, "gray4");
519 named_color<"gray5"> = parse_color(data, "gray5");
520 named_color<"gray6"> = parse_color(data, "gray6");
521 named_color<"gray7"> = parse_color(data, "gray7");
522 named_color<"gray8"> = parse_color(data, "gray8");
523 named_color<"gray9"> = parse_color(data, "gray9");
524 named_color<"gray10"> = parse_color(data, "gray10");
525
526 _accent_colors = parse_color_list(data, "accent-color");
527 _border_colors = parse_color_list(data, "border-color");
528 _fill_colors = parse_color_list(data, "fill-color");
529 _text_select_colors = parse_color_list(data, "text-select-color");
530 _primary_cursor_colors = parse_color_list(data, "primary-cursor-color");
531 _secondary_cursor_colors = parse_color_list(data, "secondary-cursor-color");
532
533 _text_style_set.clear();
534 _text_style_set.push_back({}, parse_text_style(data, "label-style"));
535 _text_style_set.push_back({phrasing_mask::warning}, parse_text_style(data, "warning-label-style"));
536 _text_style_set.push_back({phrasing_mask::error}, parse_text_style(data, "error-label-style"));
537 _text_style_set.push_back({phrasing_mask::example}, parse_text_style(data, "help-label-style"));
538 _text_style_set.push_back({phrasing_mask::placeholder}, parse_text_style(data, "placeholder-label-style"));
539
540 _margin = narrow_cast<float>(parse_int(data, "margin"));
541 _border_width = narrow_cast<float>(parse_int(data, "border-width"));
542 _rounding_radius = narrow_cast<float>(parse_int(data, "rounding-radius"));
543 _size = narrow_cast<float>(parse_int(data, "size"));
544 _large_size = narrow_cast<float>(parse_int(data, "large-size"));
545 _icon_size = narrow_cast<float>(parse_int(data, "icon-size"));
546 _large_icon_size = narrow_cast<float>(parse_int(data, "large-icon-size"));
547 _label_icon_size = narrow_cast<float>(parse_int(data, "label-icon-size"));
548
549 auto const base_font = _text_style_set.front().font_chain()[0];
550 auto const base_size = _text_style_set.front().size();
551 auto const base_cap_height = std::get<unit::points_per_em_s>(base_size) * base_font->metrics.cap_height;
552 _baseline_adjustment = ceil_in(unit::points, base_cap_height);
553 }
554
555 [[nodiscard]] friend std::string to_string(theme const& rhs) noexcept
556 {
557 return std::format("{}:{}", rhs.name, rhs.mode);
558 }
559
560 friend std::ostream& operator<<(std::ostream& lhs, theme const& rhs)
561 {
562 return lhs << to_string(rhs);
563 }
564};
565
566} // namespace hi::inline v1
Defined the color type.
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.
style_pseudo_class
The different dynamic states of a widget from the point of view of styles.
Definition style_pseudo_class.hpp:18
std::ptrdiff_t ssize_t
Signed size/index into an array.
Definition misc.hpp:32
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
font_id find_font(font_family_id family_id, font_variant variant=font_variant{}) noexcept
Find a font closest to the variant.
Definition font_book.hpp:333
font_family_id find_font_family(std::string const &family_name) noexcept
Find font family id.
Definition font_book.hpp:321
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
unit::pixel_density pixel_density
The PPI of the size values.
Definition theme.hpp:28
constexpr float border_width() const noexcept
The line-width of a border.
Definition theme.hpp:68
theme transform(unit::pixel_density new_pixel_density) const noexcept
Create a transformed copy of the theme.
Definition theme.hpp:139
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
T max(T... args)
T min(T... args)
T round(T... args)
T to_string(T... args)
T what(T... args)